手绘二维码

看到二维码,很容易猜到黑白相间的小方格就是二进制比特。那么这些比特是怎么得到的?小方格又是按照什么规则排布的?今天咱们就从零开始将一个 url 画成二维码。

考虑到大多数人可能不太了解二维码,所以先讲下基础概念。你也可以先看看左耳朵耗子写的二维码的生成细节和原理

版本

二维码一共有 40 个尺寸,官方叫作版本 Version。最小的 Version 1 是 21 × 21 的矩阵,Version 2 是 25 × 25 的矩阵 ... 每增加 1 version,就增加 4 的尺寸。最高是 177 × 177 的矩阵(Version 40)。

纠错

根据日常使用经验,无论是残缺、遮挡还是污损,二维码都能够被正确识别,这得益于二维码的纠错机制。

纠错是通过纠错码实现的,当我们将源数据编码后,会采用一种纠错算法算出编码结果对应的纠错码,连同编码结果一起填充到二维码。

规范里明确了四个纠错等级,不同等级的纠错比例不同。

每个版本的纠错码个数都是固定的,比如 version 1-L 需要 7 个纠错码,version 1-M 需要 10 个纠错码,版本越大、数据越多、等级越高,需要的纠错码就越多。

结构

二维码可以简单划分为三个部分:固定的功能区(用于定位)、固定的格式信息区(比如二维码用的纠错等级和版本信息)、数据区。

二进制数据都是严格按 8 位一组填入数据区的,每一组称为一个 codeword。如果数据区的格子数不是 8 的倍数,就会剩下留白(remainder)。每个版本的功能区、格式信息区和留白的位置及其占用的面积都是固定的。

更详细的划分可以参考规范里的这张图:

编码

理论上凡是可以被编译为二进制的数据,都可以用二维码来表示,只要有相应的解码程序即可。规范里规定了几种国际通用的编码模式:

比如 Numeric 仅支持 0 ~ 9 的数字,它的编码规则是:将数据字符串拆分为每三个一组,每组作为一个十进制的百位数,然后转化为 10 位二进制。为什么是 10 位呢,因为 2^10 = 1024,恰好能覆盖最大的百位数 999。如果最后一组只有两位,则转化为 7 位二进制,如果只有一位,则转化为 4 位二进制。

Alphanumeric 支持 45 个字符,0 ~ 9、A ~ Z 和几个特殊字符。

Alphanumeric 的编码规则是:根据上表查出每个 char 对应的 value,然后两两分组,按 A * 45 + B 算出一个值,并将这个值转化为 11 位二进制。如果最后一组只有一位,则将 value 转化为 6 位二进制。

Byte 模式的编码规则最简单,就是针对单字节字符集(例如 ISO-8859-1)的编码,每个字符恰好用 8 个比特表示。

我们还可以针对不同的数据片段采用不同的编码模式,这就是混合模式。

Bit Stream

二维码包含的二进制串具体是由以下几个部分组成的:

最开头一定是编码模式,是固定的 4 位(见编码一节的表格)。

接下来是原字符的长度,不同版本和不同编码模式用多少个比特表示长度是不一样的,具体是几个,规范里有明确的规定。毕竟二维码要在有限的空间里容纳更多的信息,就不能浪费比特,用过长的二进制去表示一个较小的值。

然后就是对数据按照指定的编码模式编译出的二进制序列。如果以上三部分的数据长度不是 8 的倍数,那么必须添零凑整(terminator)。

上述数据不见得能填满整个数据区,那么剩余的空间就用两个补齐码(11101100、00010001)交错填满。这两个补齐码是规范指定的。

以上整个比特串作为输入,用纠错算法输出特定个数的纠错码,二者相接,就是整个二维码数据区的数据了。

------------------ 分割线 ------------------

接下来,咱们就选用最小的版本,和最低的纠错等级,用 Alphanumeric 模式将 HTTP://I.MEITUAN.COM 这个字符串画成一个二维码。

Step 1 编码

查表可以得到每个字符对应的码点,然后两两分组,刚好分成 10 组:(17, 29)(29, 25)(44, 43)(43, 18)(42, 22)(14, 18)(29, 30)(10, 23)(42, 12)(24, 22)。

按照 A * 45 + B 算出:794, 1330, 2023, 1953, 1912, 648, 1335, 473, 1902, 1102。

然后将每个数字依次转化为二进制(toString(2)):01100011010 10100110010 11111100111 11110100001 11101111000 01010001000 10100110111 00111011001 11101101110 10001001110。

Alphanumeric 的 mode indicator 是 0010,在 version 1 里需要 9 个比特表示长度(20 => 000010100),将它们添加到头部:0010 000010100 01100011010 10100110010 11111100111 11110100001 11101111000

01010001000 10100110111 00111011001 11101101110 10001001110。

按 8 位一组重排,添 0 凑整:00100000 10100011 00011010 10100110 01011111 10011111 11010000 11110111

10000101 00010001 01001101 11001110 11001111 01101110 10001001 11000000

version 1-L 共有 26 个 codeword,其中有 7 个纠错码(error correction codeword),那么 data codeword 应该有 19 个,目前我们只有 16 个,因此剩下 3 个需要用补齐码凑足:00100000 10100011 00011010 10100110 01011111 10011111 11010000 11110111

10000101 00010001 01001101 11001110 11001111 01101110 10001001 11000000
11101100 00010001 11101100

然后算出 7 个纠错码:11010100 01110000 01110111 10010001 00101110 10010101 11001111。(我是用 node-qrcode 反算出来的,搞不懂那个纠错算法)。

最终我们得到了整个编码结果:

Step 2 填充功能区

这一步最简单,按照规范里的规定画即可。为了便于区分,用淡紫色表示功能区的白色区域。

Step 3 填充数据区

将数据按 8 位一组,从右下角开始蛇形往复填入 2 × 4 的矩形。具体怎么填规范里有明确规定,upwards 方向,将第一个数填入位置 7,第二个数填入位置 6 ... 这里是略有点坑的,因为不是从图中所示的 0 到 7,而是反过来的。方便起见,我们将数据倒过来,再按 0 ~ 7 的顺序依次填入。

Step 4 Masking

masking 的作用是使二维码的分布更加均匀。具体操作就是将数据区和 mask pattern 对应位置的比特做异或运算。比如数据区第 8 行第 10 列是白色 0,mask pattern 第 8 行第 10 列是黑色 1,异或运算的结果是黑色 1,那么就将数据区第 8 行第 10 列改成黑色。官方规定了八种 mask pattern(000 ~ 111),我们选用 000。

Step 5 填充格式信息

对所有的版本,格式信息都是固定的 15 位。其中两个是纠错等级,三个是 mask pattern,剩下十个是纠错码。

可以想象,没有格式信息,根本无法进行解码。因此格式信息非常重要,会重复画两次(类似于双机房备份)。

左下角那一列开头是永远固定的占位符。

格式信息也需要经历纠错码 + masking 的处理。

同样的,应该反过来,从位置 14 开始填。当然,也可以先把字符串倒过来。

最终结果:


本文写于 2019 年