二维码原理+js纯手写二维码生成

对于生活在21世纪的我们来说,二维码都是见怪不怪了,二维码在1994年就被日本公司Denso Wave的腾弘原发明,到现在都已经29个年头了,对于现在的开发者来说,二维码的生成,基本都是使用qr.js或者qrcode.js,当然也有很多的二维码生成库,但基本都是由qr.js改造而来。qr.js也是八年前的产物了,一方面证明了二维码算法的稳定,另一方面也说明了二维码产生的难度,致使到现在都没什么人来造轮子。

抱着好奇的心里,研究了几天二维码的原理,算是小小的有点理解

二维码的原理相对来说还是有点复杂,要想明白还是需要看多几篇文章,此处只记录本人的理解和学习笔记,后文尝试使用js来手写一下二维码的简单生成

目录

1. 二维码的基本知识

定位图案

功能性数据

数据码

纠错码 

2. 二维码数据编码

数字编码 

字符编码

结束符和补齐码

补齐码

纠错码

最终编码

3. 绘制二维码

定位图案

对齐图案 

时序图

格式信息 

版本信息

数据码与纠错码

 蒙版图案

3. 使用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 Specicon-default.png?t=N7T8http://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 Specicon-default.png?t=N7T8http://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的示例

数据对每个块的纠错码
1167 85 70 134 87 38 85 194 119 50 6 18 6 103 38213 199 11 45 115 247 241 223 229 248 154 117 154 111 86 161 111 39
2246 246 66 7 118 134 242 7 38 86 22 198 199 146 687 204 96 60 202 182 124 157 200 134 27 129 209 17 163 163 120 133
21182 230 247 119 50 7 118 134 87 38 82 6 134 151 50 7148 116 177 212 76 133 75 242 238 76 195 230 189 10 108 240 192 141
270 247 118 86 194 6 151 50 16 236 17 236 17 236 17 236235 159 5 173 24 147 59 33 106 40 255 172 82 2 131 32 178 236

二维码的纠错码主要是通过里德-所罗门纠错算法来实现的。对于这个算法,对于我来说是相当的复杂,所以我一时半会儿还有点没搞明白,还在学习中。

最终编码

到此都还是准备阶段,还要把数据码和纠错码的各个codewords交替放在一起。

对于数据码:把每个块的第一个codewords先拿出来按顺度排列好,然后再取第一块的第二个,如此类推。

如:上述示例中的Data Codewords如下:

块 167857013487388519411950618610338
块 224624666711813424273886221981991466
块 31822302471195071181348738826134151507
块 4702471188619461515016236172361723617236

我们先取第一列的: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

对于纠错码,也是一样:

块 121319911451152472412232292481541171541118616111139
块 28720496602021821241572001342712920917163163120133
块 314811617721276133752422387619523018910108240192141
块 423515951732414759331064025517282213132178236

和数据码取的一样,得到: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 Specicon-default.png?t=N7T8http://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
    }
数据码&纠错码 

最难的地方了,看了最长的时间,里面涉及到二进制的操作,和常规运算不太一样。主要难点有两个:

  1. 头部为4bit的编码模式,其次是8bit的数据大小,排列后还需要根据8bit生成数据码,源码不好理解,用常规思路改造了一下。
  2. 纠错码会涉及到多项式的计算,恕我太菜,这块真的懵逼了,直接复制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以及一些前辈的代码,就这也是只有数字编码的部分

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值