对于生活在21世纪的我们来说,二维码都是见怪不怪了,二维码在1994年就被日本公司Denso Wave的腾弘原发明,到现在都已经29个年头了,对于现在的开发者来说,二维码的生成,基本都是使用qr.js或者qrcode.js,当然也有很多的二维码生成库,但基本都是由qr.js改造而来。qr.js也是八年前的产物了,一方面证明了二维码算法的稳定,另一方面也说明了二维码产生的难度,致使到现在都没什么人来造轮子。
抱着好奇的心里,研究了几天二维码的原理,算是小小的有点理解
二维码的原理相对来说还是有点复杂,要想明白还是需要看多几篇文章,此处只记录本人的理解和学习笔记,后文尝试使用js来手写一下二维码的简单生成
目录
1. 二维码的基本知识
二维码另一个名称是QR Code(Quick Response Code),与传统条形码相比,可以存储更多的信息。二维码本质上是个密码算法,二维码存在 40 种尺寸,在官方文档中,尺寸又被命名为 Version。尺寸与 Version 存在线性关系:Version 1 是 21×21 的矩阵,Version 2 是 25×25 的矩阵,每增加一个 Version,尺寸都会增加 4,Version 的最大值是 40,故尺寸最大值是(40-1)*4+21 = 177,即 177 x 177 的矩阵。
尺寸 Size 与 Version 之间的线性关系用公式表达就是:Size = (Version - 1)* 4
在官方文档中可以找到这样一个示例图
二维码图形内容主要分为四部分:
定位图案
- Position Detection Pattern是定位图案,用于标记二维码的矩形大小。这三个定位图案有白边叫Separators for Postion Detection Patterns。之所以三个而不是四个意思就是三个就可以标识一个矩形了。
- Timing Patterns也是用于定位的。原因是二维码有40种尺寸,尺寸过大了后需要有根标准线,不然扫描的时候可能会扫歪了。
- Alignment Patterns 只有Version 2以上(包括Version2)的二维码需要这个东东,同样是为了定位用的。
功能性数据
- Format Information 存在于所有的尺寸中,用于存放一些格式化数据的。
- Version Information 在 >= Version 7以上,需要预留两块3 x 6的区域存放一些版本信息。
数据码
Data Code 数据码 ,存放二维码的数据内容。
纠错码
Error Correction Code 纠错码,用于容错。
接下来就是二维码的数据编码了
2. 二维码数据编码
我们先来说说数据编码。QR码支持如下的编码:
Numeric mode 数字编码,从0到9。如果需要编码的数字的个数不是3的倍数,那么,最后剩下的1或2位数会被转成4或7bits,则其它的每3位数字会被编成 10,12,14bits,编成多长还要看二维码的尺寸。
Alphanumeric mode 字符编码。包括 0-9,大写的A到Z(没有小写),以及符号$ % * + – . / : 包括空格。这些字符会映射成一个字符索引表。如下所示:
其中的SP是空格,Char是字符,Value是其索引值, 编码的过程是把字符两两分组,然后转成下表的45进制,然后转成11bits的二进制,如果最后有一个落单的,那就转成6bits的二进制。而编码模式和字符的个数需要根据不同的Version尺寸编成9, 11或13个二进制。
Byte mode, 字节编码,可以是0-255的ISO-8859-1字符。有些二维码的扫描器可以自动检测是否是UTF-8的编码。
Kanji mode 这是日文编码,也是双字节编码。同样,也可以用于中文编码。日文和汉字的编码会减去一个值。如:在0X8140 to 0X9FFC中的字符会减去8140,在0XE040到0XEBBF中的字符要减去0XC140,然后把结果前两个16进制位拿出来乘以0XC0,然后再加上后两个16进制位,最后转成13bit的编码。
以下两张表格
Table 2 是各个编码格式的“编号”,这个东西要写在Format Information中。注:中文是1101
Table 3 表示了,不同版本(尺寸)的二维码,对于,数字,字符,字节和Kanji模式下,对于单个编码的2进制的位数。在二维码的规格说明书中,还有各种各样的编码规范表。
基于原理,这里只讨论数字编码和字符编码,简单看个实例:
数字编码
在Version 1的尺寸下,纠错级别为H的情况下,编码: 01234567
1. 把上述数字分成三组: 012 345 67
2. 把他们转成二进制: 012 转成 0000001100; 345 转成 0101011001; 67 转成 1000011。
3. 把这三个二进制串起来: 0000001100 0101011001 1000011
4. 把数字的个数转成二进制 (version 1-H是10 bits ): 8个数字的二进制是 0000001000
5. 把数字编码的标志0001和第4步的编码加到前面: 0001 0000001000 0000001100 0101011001 1000011
字符编码
在Version 1的尺寸下,纠错级别为H的情况下,编码: ABCDE
1. 从字符索引表中找到 ABCDE 这五个字条的索引 (1,2,3,4,5)
2. 两两分组: (1,2) (3,4) (5)
3. 把每一组转成11bits的二进制:
(1,2) 1*45+2 等于 47 转成 0010111
(3,4) 3*45+4 等于 139 转成 10001011
(5) 等于 5 转成 0000101
4. 把这些二进制连接起来:00101111000101110000101
5. 把字符的个数转成二进制 (Version 1-H为9 bits): 5个字符,5转成 000000101
6. 在前面加上编码标识 0010 和第5步的个数编码: 0010 000000101 00101111000101110000101
结束符和补齐码
假如我们有个HELLO WORLD的字符串要编码,根据上面的示例二,我们可以得到下面的编码:
- 编码:0010
- 字符数:000001011
- HELLO WORLD编码:01100001011 01111000110 10001011100 10110111000 10011010100 001101
按8bits重排
如果所有的编码加起来不是8个倍数我们还要在后面加上足够的0,比如上面一共有78个bits,所以,我们还要加上2个0,然后按8个bits分好组:
00100000 01011011 00001011 01111000 11010001 01110010 11011100 01001101 01000011 01000000
补齐码
最后,如果如果还没有达到我们最大的bits数的限制,我们还要加一些补齐码(Padding Bytes),Padding Bytes就是重复下面的两个bytes:11101100 00010001 ,这两个二进制转成十进制是236和17,我也不知道为什么,只知道Spec上是这么写的,关于每一个Version的每一种纠错级别的最大Bits限制,可以参看QR Code Spechttp://raidenii.net/files/datasheets/misc/qr_code.pdf的第28页到32页的Table-7一表。
假设我们需要编码的是Version 1的Q纠错级,那么,其最大需要104个bits,而我们上面只有80个bits,所以,还需要补24个bits,也就是需要3个Padding Bytes,我们就添加三个,于是得到下面的编码:
00100000 01011011 00001011 01111000 11010001 01110010 11011100 01001101 01000011 01000000 11101100 00010001 11101100
上面的编码就是数据码了,叫Data Codewords,每一个8bits叫一个codeword,我们还要对这些数据码加上纠错信息。
纠错码
上文中提到了一些纠错级别,Error Correction Code Level,二维码中有四种级别的纠错,这就是为什么二维码有残缺还能扫出来,也就是为什么有人在二维码的中心位置加入图标。
错误修正容量 | |
---|---|
L水平 | 7%的字码可被修正 |
M水平 | 15%的字码可被修正 |
Q水平 | 25%的字码可被修正 |
H水平 | 30%的字码可被修正 |
首先,我们需要对数据码进行分组,也就是分成不同的Block,然后对各个Block进行纠错编码,对于如何分组,我们可以查看QR Code Spechttp://raidenii.net/files/datasheets/misc/qr_code.pdf的第33页到44页的Table-13到Table-22的定义表。注意最后两列
Number of Error Code Correction Blocks :需要分多少个块。
Error Correction Code Per Blocks:每一个块中的code个数,所谓的code的个数,也就是有多少个8bits的字节。
举个例子:上述的Version 5 + Q纠错级:需要4个Blocks(2个Blocks为一组,共两组),头一组的两个Blocks中各15个bits数据 + 各 9个bits的纠错码
注:表中的codewords就是一个8bits的byte
再注:最后一例中的c, k, r 的公式为:c = k + 2 * r,因为后脚注解释了:纠错码的容量小于纠错码的一半
下图给一个5-Q的示例
组 | 块 | 数据 | 对每个块的纠错码 |
---|---|---|---|
1 | 1 | 67 85 70 134 87 38 85 194 119 50 6 18 6 103 38 | 213 199 11 45 115 247 241 223 229 248 154 117 154 111 86 161 111 39 |
2 | 246 246 66 7 118 134 242 7 38 86 22 198 199 146 6 | 87 204 96 60 202 182 124 157 200 134 27 129 209 17 163 163 120 133 | |
2 | 1 | 182 230 247 119 50 7 118 134 87 38 82 6 134 151 50 7 | 148 116 177 212 76 133 75 242 238 76 195 230 189 10 108 240 192 141 |
2 | 70 247 118 86 194 6 151 50 16 236 17 236 17 236 17 236 | 235 159 5 173 24 147 59 33 106 40 255 172 82 2 131 32 178 236 |
二维码的纠错码主要是通过里德-所罗门纠错算法来实现的。对于这个算法,对于我来说是相当的复杂,所以我一时半会儿还有点没搞明白,还在学习中。
最终编码
到此都还是准备阶段,还要把数据码和纠错码的各个codewords交替放在一起。
对于数据码:把每个块的第一个codewords先拿出来按顺度排列好,然后再取第一块的第二个,如此类推。
如:上述示例中的Data Codewords如下:
块 1 | 67 | 85 | 70 | 134 | 87 | 38 | 85 | 194 | 119 | 50 | 6 | 18 | 6 | 103 | 38 | |
块 2 | 246 | 246 | 66 | 7 | 118 | 134 | 242 | 7 | 38 | 86 | 22 | 198 | 199 | 146 | 6 | |
块 3 | 182 | 230 | 247 | 119 | 50 | 7 | 118 | 134 | 87 | 38 | 82 | 6 | 134 | 151 | 50 | 7 |
块 4 | 70 | 247 | 118 | 86 | 194 | 6 | 151 | 50 | 16 | 236 | 17 | 236 | 17 | 236 | 17 | 236 |
我们先取第一列的:67, 246, 182, 70
然后再取第二列的:67, 246, 182, 70, 85,246,230 ,247
如此类推:67, 246, 182, 70, 85,246,230 ,247 ……… ……… ,38,6,50,17,7,236
对于纠错码,也是一样:
块 1 | 213 | 199 | 11 | 45 | 115 | 247 | 241 | 223 | 229 | 248 | 154 | 117 | 154 | 111 | 86 | 161 | 111 | 39 |
块 2 | 87 | 204 | 96 | 60 | 202 | 182 | 124 | 157 | 200 | 134 | 27 | 129 | 209 | 17 | 163 | 163 | 120 | 133 |
块 3 | 148 | 116 | 177 | 212 | 76 | 133 | 75 | 242 | 238 | 76 | 195 | 230 | 189 | 10 | 108 | 240 | 192 | 141 |
块 4 | 235 | 159 | 5 | 173 | 24 | 147 | 59 | 33 | 106 | 40 | 255 | 172 | 82 | 2 | 131 | 32 | 178 | 236 |
和数据码取的一样,得到:213,87,148,235,199,204,116,159,…… …… 39,133,141,236
然后,再把这两组放在一起(纠错码放在数据码之后)得到:
67, 246, 182, 70, 85, 246, 230, 247, 70, 66, 247, 118, 134, 7, 119, 86, 87, 118, 50, 194, 38, 134, 7, 6, 85, 242, 118, 151, 194, 7, 134, 50, 119, 38, 87, 16, 50, 86, 38, 236, 6, 22, 82, 17, 18, 198, 6, 236, 6, 199, 134, 17, 103, 146, 151, 236, 38, 6, 50, 17, 7, 236, 213, 87, 148, 235, 199, 204, 116, 159, 11, 96, 177, 5, 45, 60, 212, 173, 115, 202, 76, 24, 247, 182, 133, 147, 241, 124, 75, 59, 223, 157, 242, 33, 229, 200, 238, 106, 248, 134, 76, 40, 154, 27, 195, 255, 117, 129, 230, 172, 154, 209, 189, 82, 111, 17, 10, 2, 86, 163, 108, 131, 161, 163, 240, 32, 111, 120, 192, 178, 39, 133, 141, 236
这就是我们的数据区。
Remainder Bits
最后再加上Reminder Bits,对于某些Version的QR,上面的还不够长度,还要加上Remainder Bits,比如:上述的5Q版的二维码,还要加上7个bits,Remainder Bits加零就好了。关于哪些Version需要多少个Remainder bit,可以参看QR Code Spechttp://raidenii.net/files/datasheets/misc/qr_code.pdf的第15页的Table-1的定义表。
3. 绘制二维码
定位图案
二维码的三个定位图位于三个角,至于为什么是三个,因为三个点就可以确定一个矩形了
而且不论二维码的Version如何,他的定位点大小都是一致的
对齐图案
对齐图和定位图一样,不论Version如何,他的大小都是这样,一定是一个 5×5 的矩阵
对齐图案绘制的位置,可参看 QR Code Spec 的 Table-E.1 一表查询,部分内容如下图 6.3 所示:
下图是根据上述表格中的Version8的一个例子(6,24,42)
时序图
时序图案是两条连接三个定位图案的线,如下图 所示:
格式信息
格式信息在定位图案周围分布,由于定位图案个数固定为 3 个,且大小固定,故格式信息也是一个固定 15bits 的信息。每个 bit 的位置如下图 所示:(注:图中的 Dark Module 是固定永远出现的)
15bits 中数据,按照 5bits 的数据位 + 10bits 纠错位的顺序排列:
- 数据位占 5bits:其中 2bits 用于表示使用的纠错等级 (Error Correction Level),3bits 用于表示使用的蒙版 (Mask) 类别;
- 纠错位占 10bits:主要通过 BCH Code 计算;
为了减少扫描后图像识别的困难,最后还需要将 15bits 与 101010000010010 做异或 XOR 操作。因为我们在原格式信息中可能存在太多的 0 值(如纠错级别为 00,蒙版 Mask 为 000),使得格式信息全部为白色,这将增加分析图像的困难。
纠错等级的编码如下图的表格所示:
假设存在纠错等级为 M(对应 00),蒙版图案对应 000,5bits 的数据位为 00101,10bits 的纠错位为 0011011100: 则生成了在异或操作之前的 bits 序列为:001010011011100 与 101010000010010 做异或 XOR 操作,即得到最终格式信息:100000011001110
版本信息
对于 Version 7 及其以上的二维码,需要加入版本信息。如下图 蓝色部分所示:
版本信息依附在定位图案周围,故大小固定为 18bits。水平竖直方向的填充方式如图
18bits 的版本信息中,前 6bits 为版本号 (Version Number),后 12bits 为纠错码 (BCH Bits)。
假设存在一个 Version 为 7 的二维码(对应 6bits 版本号为 000111),其纠错码为 110010010100; 则版本信息图案中的应填充的数据为:000111110010010100
数据码与纠错码
此后即可填充前面得到的数据内容了。填充的思想如下图所示,从二维码的右下角开始,沿着红线进行填充,遇到非数据区域,则绕开或跳过。
然而这样难以理解,我们可以将其分为许多小模块,然后将许多小模块串连在一起,如下图
小模块可以分为常规模块和非常规模块,每个模块的容量都为 8。常规情况下,小模块都为宽度为 2 的竖直小矩阵,按照方向将 8bits 的码字填充在内。非常规情况下,模块会产生变形。 填充方式上图 6.14,图中深色区域(如 D1 区域)填充数据码,白色区域(如 E15 区域)填充纠错码。遍历顺序依旧从最右下角的 D1 区域开始,按照蛇形方向(D1→D2→…→D28→E1→E2→…→E16→剩余码)进行小模块的填充,并从右向左交替着上下移动,填充有如下原则:
原则 1:无论数据的填充方向是向上还是向下,常规模块(即 8bits 数据全在两列内)的排列顺序应是从右向左,如下图 6.15所示;
原则 2:每个码字的最高有效位(即第7个bit)应置于第一个可用位。对于向上填充的方向,最高有效位应该占据模块的右下角;向下填充的方向,最高有效位占据模块的右上方。 注:对于某些模块(以下图 6.17 为例),如果前一个模块在右边模块的列内部结束,则该模块成为不规则模块,且与常规模块相比,原本填充方向向上时,最高位应该在右上角,此时则变为左下角;
原则 3:当一个模块的两列同时遇到对齐图案或时序图案的水平边界时,它将继续在图案的上方或下方延续;
原则 4:当模块到达区域的上下边界(包括二维码的上下边界、格式信息、版本信息或分隔符)时,码字中任何剩余 bits 将填充在左边的下一列中,且填充方向反转
原则 5:当模块的右一列遇到对齐图案,或遇到被版本信息占据的区域时,数据位会沿着对齐图案或版本信息旁边的一列继续填充,并形成一个不规则模块。如果当前模块填充结束之前,下一个的两列都可用,则下一个码字的最高有效位应该放在单列中
蒙版图案
这样下来,我们的图就填好了,但是,也许那些点并不均衡,如果出现大面积的空白或黑块,会告诉我们扫描识别的困难。所以,我们还要做Masking操作(靠,还嫌不复杂)QR的Spec中说了,QR有8个Mask你可以使用,如下所示:其中,各个mask的公式在各个图下面。所谓mask,说白了,就是和上面生成的图做XOR操作。Mask只会和数据区进行XOR,不会影响功能区。(注:选择一个合适的Mask也是有算法的)
蒙版操作的过程与对比图如下图 所示,图中最上层是没有经过蒙版操作的原始二维码,其中存在大量黑色区域,难以后续的分析识别。经过两种不同蒙版的处理,可以看到最后生成的二维码变的更加混乱,容易识别。
蒙版操作之后,得到的二维码即为最终我们平常看到的结果。
至此,二维码就绘制完成了,以上就是二维码的基本原理,其中纠错码部分我感觉要比以上内容加起来还要复杂
3. 使用js生成二维码的实现
为了开发以及调试的简单,我在node环境中尝试,最后绘制选择输出HTML字符串写入到HTML文件中
绘制方法
text.js为主要代码的实现,最后生成的绘制数据应该为一个二维数组,数组元素为0和1,所以主要的代码实现为
function print(data) {
//传入的data是一个二维数组,每一项是一个数组,代表一行,每一项是0或1,代表黑白,0是白色div,1是黑色div,输出一段HTML字符串
let width = '3px' // 每个div的宽度
let height = '3px' // 每个div的高度
let html = ''
for (let i = 0; i < data.length; i++) {
let row = data[i]
for (let j = 0; j < row.length; j++) {
let color = row[j] === 1 ? 'black' : 'white'
html += `<div style="width:${width};height:${height};background-color:${color};float:left;"></div>`
}
html += '<div style="clear:both;"></div>'
}
//然后将html字符串写入到./index.html中的body中
const fs = require('fs')
const path = require('path')
const htmlPath = path.resolve(__dirname, './index.html')
const htmlStr = fs.readFileSync(htmlPath, 'utf-8')
const start = htmlStr.indexOf('<body>')
const end = htmlStr.indexOf('</body>')
const newHtmlStr =
htmlStr.slice(0, start + 6) + html + htmlStr.slice(end)
fs.writeFileSync(htmlPath, newHtmlStr)
}
制作好HTML字符串之后,使用node将这一段HTML写入index.html文件中,浏览器刷新即可得到挥着的二维码
主题思路和代码
因为设想是完成一个简单版本的二维码生成器
初始化
因为二维码有较多版本,不同版本的尺寸不一样,本想实现 version1 版本,但过于简单,连个对齐图案都没有,故就实现了 version2版本,使用的字节编码(Byte Mode)模式,不同模式生成二维码对应的二进制位数不一样。在初始化时就设定好版本,同时创建一个modules属性存储二维码图案信息,使用 1 标识黑块,0 标识白块,null标识还未被处理,吐血的经验,图案信息需要三个状态
,一开始实现我都初始化为 0,后面处理数据码和纠错码时,出了一堆问题
constructor(data) {
this.dataMode = 1 << 2 // 使用8bit-byte格式
this.version = 2 // 使用version2 25 * 25 尺寸
this.size = this.version * 4 + 17
this.maskPattern = 7
this.errorCorrectLevel = 1 // 1-L-01 0-M-00 3-Q-11 2-H-10
// 生成二维数组存储数据
this.modules = new Array(this.size)
for (let i = 0; i < this.size; i++) {
this.modules[i] = new Array(this.size).fill(null)
}
this.data = data
}
定位图案的实现
定位图案是分别在左上、右上、左下三个角的方块,有固定的尺寸大小,
在实现时,千万不能忘记了定位图片需要被白色块包裹,也就是理论准备中二维图中的分割器(Separators fro Position Detection Patterns),此处又踩了个坑。实现方式也相对源码取巧,生成一个二维数组标识方块,如下图,然后遍历该二维数组,设置对应modules属性中位置
setPositionDetectionPattern() {
// 还需要处理各个白边情况
const detection = [
[1, 1, 1, 1, 1, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
]
// 根据detection数组替换modules中对应位置的 null -> 0 / 1
for (let r = 0; r <= 7; r++) {
const positions = detection[r]
const row = this.modules[r]
const lastRow = this.modules[this.size - r - 1]
for (let c = 0; c <= 7; c++) {
const val = positions[c]
const lastCol = this.size - c - 1
// 左上角
row[c] = val
// 右上角
row[lastCol] = val
// 左下角
lastRow[c] = val
}
}
}
同理,对齐图案因为也是一个固定大小的方块,实现方式也类似
setAlignmentPattern() {
const POSITION_TABLE = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34]]
const [row, col] = POSITION_TABLE[this.version - 1]
// 修改位置
if (!row || !col) {
throw new Error('版本', this.version, '不支持')
}
// [6, 18]表示的是组合,因为右上,坐上,左下都已经有定位图案,故只需要考虑 右下一种组合
// 对齐图案的中心点为 col 和 col
// 对应的起点位置为 col - 2, col - 2
const alignment = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
]
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
const pRow = r + col - 2
const pCol = c + col - 2
this.modules[pRow][pCol] = alignment[r][c]
}
}
}
时序图案实现
时序图案就是定位图片三个角连成的两条线,应该是里面最简单的一个
setTimingPattern() {
for (let i = 8; i < this.size - 8; i++) {
// 正好是下标为偶数时需要为1
const flag = (i - 1) % 2
// 设置连接顶部两个定位图案的线
this.modules[6][i] = flag
// 设置连接左边两个定位图案的线
this.modules[i][6] = flag
}
}
版本&格式信息
版本和格式信息一共 15 bits,包含 5bits 的数据位 + 10bits 纠错位。其中数据位为 2bits 纠错等级 + 3bits蒙版。一共会有两个位置会存储,如下图的左边那个。涉及到纠错位的生成,开始需要用到些数学知识,有点难,没有理解完全,照着 qr.js
中的实现改造了些,代码就直接看源码吧
setTypeInfo() {
// 数据位为 2bits 纠错等级 + 3bits蒙版
// 通过位操作,a<<b a向左移动b位,例如 01 << 3 => 01000,后面三位位蒙版 可以通过 或操作直接添加
const data = (this.errorCorrectLevel << 3) | this.maskPattern
// 通过数据位 计算纠错位
const bits = this.getBCHTypeInfo(data)
// 设置格式信息,需要横向和纵向设置两次
for (let i = 0; i < 15; i++) {
// 当前位置的值
const flag = (bits >> i) & 1
// 横向设置
if (i < 8) {
this.modules[8][this.size - i - 1] = flag
} else if (i < 9) {
// 8 位置前还有一个对齐线
this.modules[8][15 - i - 1 + 1] = flag
} else {
this.modules[8][15 - i - 1] = flag
}
// 设置纵向
if (i < 6) {
this.modules[i][8] = flag
} else if (i < 8) {
this.modules[i + 1][8] = flag
} else {
this.modules[this.size - 15 + i][8] = flag
}
}
// 有个固定位置的黑块
this.modules[this.size - 8][8] = 1
}
getBCHTypeInfo(data) {
// 有10bits的纠错位
let d = data << 10
// 计算二进制数据的最大位数,
const getBCHDigit = (num) => {
let digit = 0
while (num != 0) {
digit++
// 无符号右移, num = num >>> 1, 可以简单理解为抛弃小数位的除法操作
num >>>= 1
}
return digit
}
// 不知道为啥要定义 G15 和 G15_MASK
const G15 =
(1 << 10) |
(1 << 8) |
(1 << 5) |
(1 << 4) |
(1 << 2) |
(1 << 1) |
(1 << 0)
const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
const G15Digit = getBCHDigit(G15)
while (getBCHDigit(d) - G15Digit >= 0) {
d ^= G15 << (getBCHDigit(d) - G15Digit)
}
// 最终得到的d为 10 的纠错码
return ((data << 10) | d) ^ G15_MASK
}
数据码&纠错码
最难的地方了,看了最长的时间,里面涉及到二进制的操作,和常规运算不太一样。主要难点有两个:
- 头部为4bit的编码模式,其次是8bit的数据大小,排列后还需要根据8bit生成数据码,源码不好理解,用常规思路改造了一下。
- 纠错码会涉及到多项式的计算,恕我太菜,这块真的懵逼了,直接复制
qr.js
源码的中实现。 使用下述函数生成对应位数的二进制编码
function getBinary(num, length) {
const long = '00000000000000000000000000000000' // 长度没啥具体含义,保证比 length大就好
const binary = num.toString(2)
if (binary.length < length) {
return long.slice(0, length - binary.length) + binary
}
return binary
}
后面的纠错码也是懵逼状态,涉及到多项式,蒙版图案,可能是文案太简单,试了试不同蒙版(对应源码中的maskPattern字段,可0-7共8中模式),也都差不多
createData() {
// 实现的是 2-L 版本
const rsBlocks = { totalCount: 44, dataCount: 34 }
// 生成二进制数据
let binaryData = ''
// 编码数据格式
binaryData += getBinary(this.dataMode, 4)
// 数据长度
binaryData += getBinary(this.data.length, 8)
// 写入数据
for (let i = 0; i < this.data.length; i++) {
binaryData += getBinary(this.data.charCodeAt(i), 8)
}
const dataCount = rsBlocks.dataCount * 8
if (binaryData.length > dataCount) {
throw new Error('code length overflow')
}
// 结束符
if (binaryData.length <= dataCount + 4) {
binaryData += getBinary(0, 4)
}
// 不足8位补齐一下0
if (binaryData.length % 8) {
binaryData += getBinary('0', 8 - (binaryData.length % 8))
}
// 数据长度不够,用 0xec 和 0x11补齐
while (true) {
if (binaryData.length >= dataCount) {
break
}
binaryData += getBinary(0xec, 8)
if (binaryData.length >= dataCount) {
break
}
binaryData += getBinary(0x11, 8)
}
// 结合多项式生成最终的二维码数据
// 通过二进制,生成数据
const buffer = new Array(rsBlocks.dataCount)
for (let i = 0; i < buffer.length; i++) {
buffer[i] = 0xff & Number('0b' + binaryData.slice(i * 8, (i + 1) * 8))
}
const rsPoly = getErrorCorrectPolynomial(
rsBlocks.totalCount - rsBlocks.dataCount
)
const rawPoly = new Polynomial(buffer, rsPoly.getLength() - 1)
const modPoly = rawPoly.mod(rsPoly)
const errorBuffer = new Array(rsPoly.getLength() - 1)
for (let i = 0; i < errorBuffer.length; i++) {
const modIndex = i + modPoly.getLength() - errorBuffer.length
errorBuffer[i] = modIndex >= 0 ? modPoly.get(modIndex) : 0
}
// 生成数据
return [...buffer, ...errorBuffer]
}
生成二维码
const qrcode = new QRCode('123456')
//得到二维码数据数组
let modules = qrcode.make()
//绘制二维码到HTML文件中
print(modules)
完整代码
//实现一个最简单的二维码生成器 2-L模式
const EXP_TABLE = new Array(256)
const LOG_TABLE = new Array(256)
function initMathTable() {
for (let i = 0; i < 8; i++) {
EXP_TABLE[i] = 1 << i
}
for (let i = 8; i < 256; i++) {
EXP_TABLE[i] =
EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8]
}
for (let i = 0; i < 255; i++) {
LOG_TABLE[EXP_TABLE[i]] = i
}
}
initMathTable()
function getMathLog(n) {
return LOG_TABLE[n]
}
function getMathExp(n) {
n = n % 255
if (n < 0) {
n += 255
}
const exp = EXP_TABLE[n]
if (exp === undefined) {
console.log('n 有异常:', n)
}
return exp
}
class Polynomial {
constructor(num = [], shift) {
let offset = 0
while (offset < num.length && num[offset] === 0) {
offset++
}
this.num = new Array(num.length - offset + shift)
for (let i = 0; i < num.length - offset; i++) {
this.num[i] = num[i + offset]
}
}
get(index) {
return this.num[index]
}
getLength() {
return this.num.length
}
multiply(e) {
let num = new Array(this.getLength() + e.getLength() - 1)
for (let i = 0; i < this.getLength(); i++) {
for (let j = 0; j < e.getLength(); j++) {
const log = getMathLog(this.get(i)) + getMathLog(e.get(j))
num[i + j] ^= getMathExp(log)
}
}
return new Polynomial(num, 0)
}
mod(e) {
if (this.getLength() - e.getLength() < 0) {
return this
}
let ratio = getMathLog(this.get(0)) - getMathLog(e.get(0))
let num = new Array(this.getLength())
for (let i = 0; i < this.getLength(); i++) {
num[i] = this.get(i)
}
for (let i = 0; i < e.getLength(); i++) {
num[i] ^= getMathExp(getMathLog(e.get(i)) + ratio)
}
return new Polynomial(num, 0).mod(e)
}
}
class QRCode {
constructor(data) {
this.dataMode = 1 << 2 // 使用8bit-byte格式
this.version = 2 // 使用version2 25 * 25 尺寸
this.size = this.version * 4 + 17
this.maskPattern = 7
this.errorCorrectLevel = 1 // 1-L-01 0-M-00 3-Q-11 2-H-10
// 生成二维数组存储数据
this.modules = new Array(this.size)
for (let i = 0; i < this.size; i++) {
this.modules[i] = new Array(this.size).fill(null)
}
this.data = data
}
make() {
// 定位图案
this.setPositionDetectionPattern()
// 对齐图案
this.setAlignmentPattern()
// 时序图案
this.setTimingPattern()
// 版本信息&格式信息
this.setTypeInfo()
// // 数据码&纠错码
this.mapData(this.createData())
return this.modules
}
// 三个角上面的定位图片, 左上,右上,左下
/*
[1, 1, 1, 1, 1, 1, 1]
[1, 0, 0, 0, 0, 0, 1]
[1, 0, 1, 1, 1, 0, 1]
[1, 0, 1, 1, 1, 0, 1]
[1, 0, 1, 1, 1, 0, 1]
[1, 0, 0, 0, 0, 0, 1]
[1, 1, 1, 1, 1, 1, 1]
*/
setPositionDetectionPattern() {
// 还需要处理各个白边情况
const detection = [
[1, 1, 1, 1, 1, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
]
// 根据detection数组替换modules中对应位置的 null -> 0 / 1
for (let r = 0; r <= 7; r++) {
const positions = detection[r]
const row = this.modules[r]
const lastRow = this.modules[this.size - r - 1]
for (let c = 0; c <= 7; c++) {
const val = positions[c]
const lastCol = this.size - c - 1
// 左上角
row[c] = val
// 右上角
row[lastCol] = val
// 左下角
lastRow[c] = val
}
}
}
// 对齐图案,位置由 POSITION_TABLE 提供, 形式为
/*
[1,1,1,1,1]
[1,0,0,0,1],
[1,0,1,0,1]
[1,0,0,0,1]
[1,1,1,1,1]
*/
setAlignmentPattern() {
const POSITION_TABLE = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34]]
const [row, col] = POSITION_TABLE[this.version - 1]
// 修改位置
if (!row || !col) {
throw new Error('版本', this.version, '不支持')
}
// [6, 18]表示的是组合,因为右上,坐上,左下都已经有定位图案,故只需要考虑 右下一种组合
// 对齐图案的中心点为 col 和 col
// 对应的起点位置为 col - 2, col - 2
const alignment = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
]
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
const pRow = r + col - 2
const pCol = c + col - 2
this.modules[pRow][pCol] = alignment[r][c]
}
}
}
// 时序图案,连接三个定位图案的线
setTimingPattern() {
for (let i = 8; i < this.size - 8; i++) {
// 正好是下标为偶数时需要为1
const flag = (i - 1) % 2
// 设置连接顶部两个定位图案的线
this.modules[6][i] = flag
// 设置连接左边两个定位图案的线
this.modules[i][6] = flag
}
}
// 设置二维码的格式信息,包含 5bits 的数据位 + 10bits 纠错位
setTypeInfo() {
// 数据位为 2bits 纠错等级 + 3bits蒙版
// 通过位操作,a<<b a向左移动b位,例如 01 << 3 => 01000,后面三位位蒙版 可以通过 或操作直接添加
const data = (this.errorCorrectLevel << 3) | this.maskPattern
// 通过数据位 计算纠错位
const bits = this.getBCHTypeInfo(data)
// 设置格式信息,需要横向和纵向设置两次
for (let i = 0; i < 15; i++) {
// 当前位置的值
const flag = (bits >> i) & 1
// 横向设置
if (i < 8) {
this.modules[8][this.size - i - 1] = flag
} else if (i < 9) {
// 8 位置前还有一个对齐线
this.modules[8][15 - i - 1 + 1] = flag
} else {
this.modules[8][15 - i - 1] = flag
}
// 设置纵向
if (i < 6) {
this.modules[i][8] = flag
} else if (i < 8) {
this.modules[i + 1][8] = flag
} else {
this.modules[this.size - 15 + i][8] = flag
}
}
// 有个固定位置的黑块
this.modules[this.size - 8][8] = 1
}
getBCHTypeInfo(data) {
// 有10bits的纠错位
let d = data << 10
// 计算二进制数据的最大位数,
const getBCHDigit = (num) => {
let digit = 0
while (num != 0) {
digit++
// 无符号右移, num = num >>> 1, 可以简单理解为抛弃小数位的除法操作
num >>>= 1
}
return digit
}
// 不知道为啥要定义 G15 和 G15_MASK
const G15 =
(1 << 10) |
(1 << 8) |
(1 << 5) |
(1 << 4) |
(1 << 2) |
(1 << 1) |
(1 << 0)
const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
const G15Digit = getBCHDigit(G15)
while (getBCHDigit(d) - G15Digit >= 0) {
d ^= G15 << (getBCHDigit(d) - G15Digit)
}
// 最终得到的d为 10 的纠错码
return ((data << 10) | d) ^ G15_MASK
}
createData() {
// 实现的是 2-L 版本
const rsBlocks = { totalCount: 44, dataCount: 34 }
// 生成二进制数据
let binaryData = ''
// 编码数据格式
binaryData += getBinary(this.dataMode, 4)
// 数据长度
binaryData += getBinary(this.data.length, 8)
// 写入数据
for (let i = 0; i < this.data.length; i++) {
binaryData += getBinary(this.data.charCodeAt(i), 8)
}
const dataCount = rsBlocks.dataCount * 8
if (binaryData.length > dataCount) {
throw new Error('code length overflow')
}
// 结束符
if (binaryData.length <= dataCount + 4) {
binaryData += getBinary(0, 4)
}
// 不足8位补齐一下0
if (binaryData.length % 8) {
binaryData += getBinary('0', 8 - (binaryData.length % 8))
}
// 数据长度不够,用 0xec 和 0x11补齐
while (true) {
if (binaryData.length >= dataCount) {
break
}
binaryData += getBinary(0xec, 8)
if (binaryData.length >= dataCount) {
break
}
binaryData += getBinary(0x11, 8)
}
// 结合多项式生成最终的二维码数据
// 通过二进制,生成数据
const buffer = new Array(rsBlocks.dataCount)
for (let i = 0; i < buffer.length; i++) {
buffer[i] = 0xff & Number('0b' + binaryData.slice(i * 8, (i + 1) * 8))
}
const rsPoly = getErrorCorrectPolynomial(
rsBlocks.totalCount - rsBlocks.dataCount
)
const rawPoly = new Polynomial(buffer, rsPoly.getLength() - 1)
const modPoly = rawPoly.mod(rsPoly)
const errorBuffer = new Array(rsPoly.getLength() - 1)
for (let i = 0; i < errorBuffer.length; i++) {
const modIndex = i + modPoly.getLength() - errorBuffer.length
errorBuffer[i] = modIndex >= 0 ? modPoly.get(modIndex) : 0
}
// 生成数据
return [...buffer, ...errorBuffer]
}
mapData(data) {
let inc = -1
let row = this.size - 1
let bitIndex = 7
let byteIndex = 0
for (let col = row; col > 0; col -= 2) {
if (col == 6) col--
while (true) {
for (let c = 0; c < 2; c++) {
if (this.modules[row][col - c] == null) {
let dark = false
if (byteIndex < data.length) {
dark = ((data[byteIndex] >>> bitIndex) & 1) == 1
}
let mask = getMask(this.maskPattern, row, col - c)
if (mask) {
dark = !dark
}
this.modules[row][col - c] = Number(dark)
bitIndex--
if (bitIndex == -1) {
byteIndex++
bitIndex = 7
}
}
}
row += inc
if (row < 0 || this.size <= row) {
row -= inc
inc = -inc
break
}
}
}
}
}
function getBinary(num, length) {
const long = '00000000000000000000000000000000'
const binary = num.toString(2)
if (binary.length < length) {
return long.slice(0, length - binary.length) + binary
}
return binary
}
function getErrorCorrectPolynomial(length) {
let a = new Polynomial([1], 0)
for (let i = 0; i < length; i++) {
a = a.multiply(new Polynomial([1, getMathExp(i)], 0))
}
return a
}
function getMask(maskPattern, i, j) {
switch (maskPattern) {
case 0:
return (i + j) % 2 == 0
case 1:
return i % 2 == 0
case 2:
return j % 3 == 0
case 3:
return (i + j) % 3 == 0
case 4:
return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0
case 5:
return ((i * j) % 2) + ((i * j) % 3) == 0
case 6:
return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0
case 7:
return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0
default:
throw new Error('bad maskPattern:' + maskPattern)
}
}
function print(data) {
//传入的data是一个二维数组,每一项是一个数组,代表一行,每一项是0或1,代表黑白,0是白色div,1是黑色div,输出一段HTML字符串
let width = '3px' // 每个div的宽度
let height = '3px' // 每个div的高度
let html = ''
for (let i = 0; i < data.length; i++) {
let row = data[i]
for (let j = 0; j < row.length; j++) {
let color = row[j] === 1 ? 'black' : 'white'
html += `<div style="width:${width};height:${height};background-color:${color};float:left;"></div>`
}
html += '<div style="clear:both;"></div>'
}
//然后将html字符串写入到./index.html中的body中
const fs = require('fs')
const path = require('path')
const htmlPath = path.resolve(__dirname, './index.html')
const htmlStr = fs.readFileSync(htmlPath, 'utf-8')
const start = htmlStr.indexOf('<body>')
const end = htmlStr.indexOf('</body>')
const newHtmlStr =
htmlStr.slice(0, start + 6) + html + htmlStr.slice(end)
fs.writeFileSync(htmlPath, newHtmlStr)
}
const qrcode = new QRCode('123456')
//得到二维码数据数组
let modules = qrcode.make()
//绘制二维码到HTML文件中
print(modules)
运行效果
这些代码也是借鉴了qr.js以及一些前辈的代码,就这也是只有数字编码的部分