编码指南

生成与扫描二维码:把该知道的事搞清楚

二维码的结构、版本与编码模式如何决定容量、纠错等级到底牺牲了什么、扫不出来的常见原因,以及为什么扫一个来路不明的码等同于点一个来路不明的链接。

二维码是一种二维条码,内建的冗余足以让它在印刷质量不佳、拍摄角度倾斜、部分遮挡的情况下依然可读。生成时的大多数决定 —— 编多少内容、选哪个纠错等级、印多大 —— 彼此之间是相互影响的,而这种影响在码真正用到现场之前几乎看不出来。本文把结构讲到足够清楚,让这些决定不再靠猜。

这个方块里到底有什么

二维码符号是由模块组成的网格,也就是构成图像的黑白小方格。三个角上的大同心方块是定位图形,它们的存在是为了让解码器从任意角度找到符号并判断出旋转方向。两个定位图形之间那条黑白相间的窄线是定时图形,用来告诉解码器模块的间距。较大的符号还会加入较小的校正图形,让解码器在斜拍或曲面印刷造成透视畸变时能够纠正回来。

符号尺寸称为版本,编号 1 到 40。版本 1 是 21 × 21 个模块,之后每升一个版本每边增加 4 个模块,所以版本 N 是 17 + 4N,版本 40 是 177 × 177。最后一个结构要素其实不属于符号本身:静区,也就是四周至少 4 个模块宽的背景色留白。省掉它,是一个技术上完美的码却扫不出来的最常见原因 —— 解码器找不到符号的边界。

编码模式决定能装多少

二维码有四种数据模式,每种的打包密度不同。数字模式用 10 位编码 3 个数字。字符数字模式用 11 位编码 2 个字符,但它的字符集只有 45 个:数字、大写 A 到 Z、空格,以及 $ % * + - . / 和冒号这八个符号。字节模式每字节占 8 位、什么都能装,UTF-8 通过 ECI 标识声明。汉字模式把一个 Shift-JIS 字符压进 13 位。

实际后果是:同样的信息,写法不同,需要的符号大小可能明显不一样。含小写字母的 URL 只能走字节模式;同一个 URL 全部大写后可以使用字符数字模式,可能落到更小的版本上 —— 在印刷尺寸固定的前提下,这意味着模块更大、扫描容错更好。这么做之所以成立,是因为 URL 的协议和主机部分大小写不敏感;但路径不是,把服务器区分大小写的路径改成大写,结果会是一个扫得很顺畅然后返回 404 的码。

同一个链接,两种模式,两种符号尺寸
链接:https://example.com/promo          (25 个字符)

字节模式,纠错等级 L
  需要 25 字节
  版本 1 可装 17 字节  -> 装不下
  版本 2 可装 32 字节  -> 装得下
  => 版本 2,25 × 25 模块

改成大写:HTTPS://EXAMPLE.COM/PROMO    (25 个字符)

字符数字模式,纠错等级 L
  需要 25 个字符
  版本 1 可装 25 个字符 -> 刚好装下
  => 版本 1,21 × 21 模块

目标完全相同,模块数从 625 降到 441。印成同样的物理宽度,
每个模块大约宽 19%,而这正是决定手机能不能在一臂距离读出来的因素。

注意:协议和主机大小写不敏感,路径不是。
在多数服务器上,/PROMO 和 /promo 是两个不同的路径。

纠错是权衡,不是拉满的开关

二维码使用 Reed-Solomon 纠错,等级决定了符号中有多大比例的码字可以损坏而数据仍能恢复。共有四个等级,习惯上按大约 7%、15%、25%、30% 的恢复能力来记。这份冗余不是白给的:纠错码字占用的是同一个符号,所以提高等级要么减少可承载的数据量,要么把版本顶上去。

「永远选 H」的直觉通常是错的。在物理尺寸固定时,版本 40 的 H 级符号比版本 10 的 M 级符号模块数多得多,而每个模块也小得多。超过某个点,你是在用「抗损坏能力」换「模块小到手机摄像头分辨不出来」,码反而更难扫。印刷和包装的合理默认值是 M,只有在有具体理由时才往上调。

最站得住脚的理由是叠加 Logo。在码中央放图形,等于有意破坏一部分模块,H 级的存在就是为了吸收这部分损失。要把被遮挡的面积控制在等级预算之内 —— Logo 覆盖超过符号面积约五分之一,就是在给自己埋间歇性故障 —— 并且绝不能压住定位图形、校正图形或定时线,因为解码器定位不到的符号,再高的纠错也救不回来。

容量数字取自字节模式。可以看到同一尺寸的符号随着冗余上升,能装的数据逐级减少。

等级大致恢复能力版本 10(57 × 57)字节容量版本 40(177 × 177)字节容量适用场景
L约 7%2712953屏幕显示等不会劣化的数字场景
M约 15%2132331通用默认值:印刷、包装、文档
Q约 25%1511663会被反复触碰、磨损或户外使用的标签
H约 30%1191273叠加 Logo、曲面、工业环境

按阅读距离决定尺寸

可靠的经验法则是十比一:符号的印刷宽度至少要达到扫描距离的十分之一。半米处扫的码,大约要 5 厘米宽;三米外看的海报,大约要 30 厘米。请从阅读距离倒推尺寸,而不是从版面上恰好剩下多少空间来定。

然后核算这个比例对应的模块尺寸。用印刷宽度除以该版本的模块数:版本 2 的符号印成 5 厘米,一行 25 个模块,每个模块 2 毫米,很从容。同样是 5 厘米,版本 20 就是一行 97 个模块、每个模块约 0.5 毫米,这已经在普通印刷和普通手机摄像头的能力边缘。如果算出来偏小,就减少数据量或加大印刷尺寸,不要用纠错等级去补。

  • 静区:四周都要留至少 4 个模块宽的干净背景,每一次都要。
  • 一定要按最终尺寸、最终材料印出成品,再用至少两种机型实测,而不是看屏幕上的预览。

扫不出来的常见原因

现场失败的原因集中在少数几类,只要知道该看什么就很好定位。如果屏幕上能扫、印出来不能扫,问题在物理层面:模块尺寸、对比度、油墨扩散或承印材料。如果到处都扫不出来、包括屏幕上,问题就在符号本身或载荷里。

  • 没有静区,或者边框、色块直接贴住符号边缘 —— 最常见的一个原因。
  • 相对阅读距离而言模块太小,或者版本远大于内容实际需要。
  • 对比度不足、反色配色,或者符号上压了渐变。
  • Logo 压住了定位图形、校正图形或定时线。
  • 图片被拉伸、不是正方形:符号必须保持 1:1 的宽高比。

二维码就是一个不透明的链接

这一节值得比通常更多的重视。二维码不携带任何认证信息,也不携带任何来源证明。任何人都可以生成指向任何目标的码,图像本身也不会透露是谁做的、通向哪里。扫一个码,完全等同于点一个你看不到文字的链接 —— 而且还多了两重问题:做出「打开」这个决定的是手机,通常在匆忙之中,并且往往不在保护办公电脑的那层过滤之内。

由此而来的攻击手法直接而常见:在停车计时器、餐厅桌面、包裹、公告栏上原本合法的印刷码上贴一张贴纸。周围的场景看起来是真的,因为它本来就是真的,被替换的只有那个码。同样的手法也出现在邮件里 —— 用一个码把目标从受监控的桌面设备转移到不受管理的个人手机上。物理篡改是可以直接检查的:印刷码上多出来的一层贴纸、贴在与周围材质不一致的单独标签上的码,或者角度和周围一切都对不齐的码。

有效的防御手段都不起眼但很管用。让相机 App 先显示目标地址,看清域名再决定是否打开,注意看可注册的那一级域名,而不是 URL 里别处出现的字眼。对二维码里的短链要格外警惕,因为它把目标藏了两层。凡是通过扫描来路不明的码进入的页面,不要输入账号密码、支付信息或一次性验证码 —— 改用你本来就知道的地址手动打开站点。还要记住码能编码的不只是网址:Wi-Fi 凭据、联系人记录、电话号码和支付载荷,都可能替你预填一个你并不想执行的动作。

要点回顾

  • 版本 N 的边长是 17 + 4N 个模块,版本 1 是 21 × 21,版本 40 是 177 × 177 —— 内容允许的前提下,版本越低越好。
  • 把 URL 的协议和主机改成大写可以进入字符数字模式、缩小符号,但区分大小写的路径绝不能改。
  • 默认选 M;只有在标签会被反复触碰或需要叠加 Logo 时才升到 Q 或 H,因为尺寸固定时冗余越多、模块越小。
  • 印刷尺寸按阅读距离的十分之一来定,四周务必留 4 个模块的静区,并在真实机型和真实材料上实测。
  • 把扫描陌生码当成点击陌生链接:先看域名再打开,通过这种方式进入的页面不要输入任何凭据,印刷码要检查有没有被贴纸覆盖。

继续了解相关检查与工具