二维码的秘密(生成原理)

时间:2022-11-17 10:10:26

我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。
文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。

这里所说的二维码默认指的就是我们经常用到的微信二维码这样方形格式的,二维码的英文全称是 Quick Response Code,快速响应矩阵图码。

我们简直不要太熟悉了,平均每天都要扫码好几次,做核酸扫码、吃饭付钱扫码、进出办公楼扫码。

二维码的秘密(生成原理)

就像上面这样,二维码就是用黑白或者明暗的格子来分别代表1和0,用黑色代表1,白色代表0,将0和1连接成二进制串,就能幻化出各种各样的内容,当然其中要经过各种算法、编码、解码等操作。

这是公众号的二维码,里面存储的内容其实就是一个链接地址,也就是一段文本内容,然后用微信扫码或者直接长按识别后,经过微信二维码识别程序的处理,将其中的内容读取出来,并且经过一系列的检测,判断这是微信公众号的二维码,于是跳转到公众号主页。

那二维码存储的不都是文本吗,不管是链接还是什么其他的东西,都是将二维码解析之后再做处理,是链接就跳转、是纯文本就显示。是这样没错,但是毕竟二维码最后是以图片的形式展示,还是要越小越好。

所以,二维码在最初设计的时候就制定了几种模式,每种模式都用最简便的编码方式处理,保证最后出来的二维码最简单。

二维码的几种模式

二维码说白了其实就是一种协议,和我们熟知的 IP、HTTP 协议类似,都是规定好一个标准,比如 IP 的头部信息,用4bit表示版本号,4bit表示首部长度等。

用4个bit标记当前二维码所采用的的模式,反应到二维码上就是4个格子。

数字编码模式

此模式对应的场景是内容全部都是数字的情况,标记为0001。

二维码的秘密(生成原理)

字符编码模式

内容包含数字和大写的A-Z(不包含小写)、以及$ % * + – . / : 和空格,标记为 0010 。

字节编码模式

支持0x00~0xFF内所有的字符,标记为 0100。

Kanji mode

日文模式,因为二维码是日本的工程师发明的,双字节编码,这个模式下也可以支持中文,标记为 1000 。

中文

支持中文,一个中文占3个字节,标记为 1101 。

混合模式

同时支持多种编码格式,可以包含中文、应为、数字等内容,标记为 0011 。

ECI

用于特殊字符,标记为 0111 。

FNC1

主要是给一些特殊的工业或行业用的,比如GS1条形码之类的。

二维码版本的概念

二维码版本是从0到40,每增加一个版本,在现有基础上横向增加4行、纵向增加4列,计算公式为(V - 1) * 4 + 21,其中V是版本号,比如版本是1,就是横竖各21个格子,版本号是2,就是横竖各25个格子。

例如下图,其中内圈白色的版本号为1,格子数为 21 x 21,外圈加上了黄色的框,版本号为2,利用公式计算,其格子数为 25 x 25,横竖各增加了 4 个。

二维码的秘密(生成原理)

版本越大,格子数越多,包含的信息量就越大,但同时生成和识别所需的时间也就越长,而且如果二维码的面积不变,那生成的格子也就越密集。

同样都是一个英文字符 A的二维码,左边版本号为1,右边版本号为40,清晰程度一目了然。

二维码的秘密(生成原理)

容错率

我们都知道,二维码在遮住其中的某些部分,或者不太清楚的情况下, 大多数时候仍然能识别出来的,这其实是和二维码本身设计机制中的容错率有关的。

下面两个二维码,左边遮挡了一小部分,右边遮挡了比较大的部分,左边就可以正常的识别出来,而右边的无论用什么客户端也识别不出来。

二维码的秘密(生成原理)

我们平时生活中也能碰到这种情况,比如之前骑共享单车的时候,有的二维码被刮了一点,但是还是可以扫出来的,但是有的二维码被刮花了很大不一部分,无论扫多长时间也于事无补。

容错率有4个等级,容错等级越高可修复的面积就越大,最高的修复率达到30%,也就是二维码有将近 1/3的面积被覆盖了,仍然能识别出来,当然不包括一些关键的区域,接下来会提到。

L 级

约 7 %的字码可被修复

M 级

约 15%的字码可以被修复

Q 级

约 25%的字码可以被修复

H 级

约 30%的字码可以被修复

一般场景下,容错率都在15%左右,因为容错等级越高,代价就越大,为了修复被遮盖的部分,只能加入更多的冗余信息。

字码是二维码中定义的一个概念,其实就是 8个bit,一个字节的单位。

最大容量

决定二维码的最大容量的因素有两个,一个是版本,一个是容错率。

版本越大,其格子数越多,所以可以编码的二进制位就越多,最终所能存储的数据量也就越多。

容错率越低,其中所冗余的用于纠错的信息就越少,所以最终真正的数据内容也就越大,因为总容量是固定的。

下面表格是版本号40(最大版本号)、容错率为L(最低容错)的情况下,不同的内容类型所能承载的最大容量,数字最多是7089个,而采用 UTF-8 编码的中文最多就984个。

内容类型 (对于版本40)
数字 最多7,089字符
字母 最多4,296字符
二进制数(8 bit) 最多2,953 字节
日文汉字 最多1,817字符(采用Shift JIS)
中文汉字 最多984字符(采用UTF-8)

现实情况中,一般也不会用二维码做大量的存储,我们可以看一下包含大量内容的二维码最后的生成效果。已经给出提示了,手机和扫码枪不易扫码。

二维码的秘密(生成原理)

生成步骤

用最简单的版本为1的二维码举例。

三个回型定位区域

这三个回型的区域可以帮助扫码器(例如微信或扫码枪)定位二维码的,与三个回型定位区域紧挨的格子要留白,也就是下图蓝色线所在的格子。

并且最后在二维码的周围要有一定的留白空间,这样可以帮助扫码器快速的定位二维码的整个区域。

二维码的秘密(生成原理)

在版本大于1的时候,还有一个小的定位块,在靠右下的蓝色框位置。

二维码的秘密(生成原理)

定位线

两条用来定位的定位线,一般用在版本比较高的二维码上。

二维码的秘密(生成原理)

填充格式数据

有一些固定的格子是用来存储当前二维码的格式信息的,比如版本号、容错级别、编码模式。下图中的蓝色格子,分别有两组15bit的区域,这两部分就用来存储当前二维码的格式信息,以二进制串的形式出现,不同的容错级别以及其他设置不同,从而计算出来的二进制串也不同。这部分计算有专门的算法,也可以通过查表的方式获取,因为总共的组合方式是固定的,也就是几十种。

这部分的数据因为有容错率的设计,所以其实也是有冗余的,不光只有内容数据容错,格式数据如果有部分遮挡的情况,也应该能识别出来。

二维码的秘密(生成原理)

如果版本大于6的话,会另外在右上和左下的回型区域的旁边开辟两块3 x 6 的区域,用来存储更多的格式信息。如下图的红色格子。

二维码的秘密(生成原理)

填充内容数据

填充完格式数据后,剩下的就是真正的内容数据了,不同的模式有不同的计算方式。

例如字符模式。
1、拆成字符对:HE, LL, O (空格), WO, RL, D
2、参考字母数字表,转换成对应的数字,然后用第一个字符数字*45+第二个字符数字,再转换成二进制,一个字符补满6位,两个字符补满11位。例如:HE=(45*17)+14=779,再转换成二进制 779 → 01100001011。为什么要乘以45呢,就是这么约定的。

确定了模式和容错级别,就能确定二维码所能容纳的最大值,如果数据量较少,没有达到最大值,就要在内容二进制串后面加上四个0bit位 0000,用来标示真正内容的结束。

如果仍然不够最大长度,则在后面连续加入 236和17的二进制串 11101100 00010001,至于为什么是这两个数字,没有为什么,就这么约定的。

最后将计算出来的二进制串从右下角开始依次填充,每次跨两列。跳过回型定位区、定位线,以及个数数据区域。遇到1就填充成黑色或者深色,遇到0就填充上白色或者浅色。

二维码的秘密(生成原理)

掩码美化

其实上一步生成好的二维码就已经可以用了,但是数据都非常集中,所以最后的出来的二维码黑色块和白色块分配严重不均,最后的效果就不太好。比如下面这样的

二维码的秘密(生成原理)

为了颜值,所以基本上在生成最终二维码之前都会加上掩模这一步。其实就是和8种固定的图案(被称作掩码图案)中的一种做异或计算,最终就可以得到一个分布均匀且比较美观的最终形态。下面是这8种掩码图案。

二维码的秘密(生成原理)

最后的效果就是我们经常看到的那些二维码。

还可以到一些在线的平台对生成的二维码进行美化,最后的呈现的效果可能像下面这样,不过美化的太过,识别起来就比较慢了,不信你可以识别下面的二维码感受一下。

二维码的秘密(生成原理)

参考文档:https://www.nayuki.io/page/creating-a-qr-code-step-by-step

公众号「古时的风筝」,Java 开发者,全栈工程师,bug 杀手,擅长解决问题。
一个兼具深度与广度的程序员鼓励师,本打算写诗却写起了代码的田园码农!坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

二维码的秘密(生成原理)