余数占了这枚字体的大半
Most of This Typeface Is Remainder
Jim Paris 做了一枚真正的 TrueType 字体。你在输入框里打 [hello],套上这枚字体,方括号里的东西当场变成一个可以扫的 QR 码。没有生成图片这一步,也没有任何脚本在旁边算。QR 码是排版引擎做字形整形(shaping)的那一瞬间画出来的——就是把 f 和 i 合成 fi 的那一层,就是让阿拉伯字母彼此连笔的那套机器。而且因为结果仍然是文本,你可以把整个 QR 码选中、复制、粘进一个纯文本文件里;你带走的是那串字符本身。
他一共发了三枚:QR Font 1-L(21×21 模块,最多 17 个字符)、2-L(25×25,32 个)、3-L(29×29,53 个)。没有一笔是手画的。一个 Python 脚本吐出字形轮廓和 GSUB 特性逻辑,再编译成 ttf。方括号之外那些照常可读的字,是 Liberation Sans 缩放进 QR 的 em 方框里——因为 Liberation 在字体许可里是保留名,他只好给自己的字族另起了名字。
值得停一下,看看这枚字体的重量压在哪里。一个 1-L 符号里,152 位是数据,56 位是 Reed–Solomon 纠错位。那 56 位不携带任何信息。它们是一次多项式除法的余数,唯一的职责就是待在那里:等到符号被撕掉一角、被杯底洇掉一块的时候,剩下的部分还能把丢掉的算回来。
而 Paris 这枚字体的机器,几乎整个是为了产出这 56 位。把字符扫成字节、填长度字段、展开成比特——这几步很轻。纠错那一段,照他自己的设计文档:152 个大型上下文替换 lookup,约四千个 parity 翻转动作,几千个中间字形。README 里甚至留了一个专门跳过它的编译目标,make fast-placeholder,注释写着「layout-only build with placeholder zero parity」——余数全填零。那样编出来的字体照样画得出一片黑白方阵,只是扫不出来。
也就是说:让一个 QR 码成其为码的,恰恰是它不说话的那部分。而这枚字体,按机器的分量算,几乎整个就是那部分。
做法本身很漂亮。他没有在字体里实现 GF(256) 上的多项式除法——他先在字体外面把这件事线性化了。对固定的版本和纠错等级,56 位 parity 是 152 位数据在 GF(2) 上的一个线性变换。喂给一个现成的编码器 152 个测试向量,每次只置 1 一位,记下回来的 56 位,那就是矩阵的第 i 列。然后每一个数据位编译成一条上下文规则:当 D37 是 1,翻转 P2、P5、P8、P11……
这里有一条很硬的边界,而他把它摆在明处。QR 标准要求编码器给八种掩码逐个打分,挑最不难看的那个。Paris 直接把掩码 0 焊死,烤进字形轮廓里,理由写得很干脆:自动选掩码要再展开一大堆布尔电路,「probably exceed the Reed–Solomon portion in complexity」。一台只会做替换的机器可以算出余数,却没法比较两个未来。把这句话说出来,比绕过去更有分量。
画面这一层是同一个路数。每个比特位有两个字形,D37_0 和 D37_1:一个空的,一个在那个模块的坐标上带一个实心方块——掩码位是 1 的地方,两者对调。所有中间字形的前进宽度都是零,于是它们全部叠在同一个原点上。最后一个 QR_BASE 字形负责定位图案、时序图案、格式位、静区,以及整串里唯一一个非零的宽度。最终那张图,用他的说法,是「几百个小字形轮廓的并集」。
流水线的顺序会在这里留下伤口。浏览器是先按 Unicode 断行、再去叫整形器的,所以它不可能知道这段文字待会儿要变成一个 QR 码。Chrome 在空格或点号处把它拆成两段分别整形,后半段没有开头那个 [,于是老老实实地渲染成 coded]。还有一道更安静的疤:他把 GDEF 表整个删掉了,因为 Firefox 会拿零宽度的 mark 字形去和最近的 base 字形做次像素吸附,parity 那一段就相对数据那一段横向漂走。这枚字体有一部分,是由一处删除定义的。
QR 码是彻底的已构。ISO/IEC 18004,基础设施,连「QR 艺术」都做成一个有名字的门类很多年了。字体设计是已构。连「OpenType 整形其实可以拿来计算」也不新鲜——Paris 自己在设计文档里顺手提了一句,2026 年有人在 TrueType hinting 的虚拟机里实现了一个光线投射器。已构之美是上一轮凿构循环的沉积物:好看,但已经不长了。
还没有名字的,是眼前这个东西。它不是一款字体——没有人会拿它排一本书。它不是一个程序——它不执行。它也不是网络艺术——它躺在一个二十几次提交的仓库里,作者是个懒得给自己写主页的工程师。(设计文档 design.md 读起来像一次对话的记录,引用链接上还留着 ChatGPT 的来源参数。这不改变这件东西本身是什么。)艺术界迟早会给「会算术的字体」想出一个词,到那天它就归了档。现在还没有。
现在去看的理由很朴素。纠错位一直就在你眼前。你每一次扫码都在看它——一个 1-L 符号的 208 个数据模块里有 56 个,一句话都不说。要让人看见余项,通常得把它单独拎出来、架到台子上。Paris 反着做:他造了一台几乎全部为了余数而运转的机器,于是余数第一次露出了自己的体量。
qr.jim.sh ↗Jim Paris has made a real TrueType font. You type [hello] into a text field, apply the font, and the brackets become a scannable QR code. There is no image-generation step and nothing computing the code off to one side. The QR code is drawn during shaping — the same stage of the text pipeline that fuses f and i into fi, the same machinery that joins Arabic letters to each other. And because the result is still text, you can select the QR code, copy it, and paste it into a plain text file. What you carry away is the string.
There are three fonts: QR Font 1-L (21×21 modules, up to 17 characters), 2-L (25×25, 32), 3-L (29×29, 53). None of them was drawn by hand. A Python script emits the glyph outlines and the GSUB feature logic, and a compiler turns that into a .ttf. The ordinary readable text outside the brackets is Liberation Sans, scaled into the QR em square — "Liberation" is a reserved name under the licence, so he had to rename his families.
It is worth stopping to see where this font's weight sits. In a Version 1-L symbol, 152 bits are data and 56 bits are Reed–Solomon parity. The 56 carry nothing. They are the remainder of a polynomial division, and their only job is to be there so that when a corner is torn off, or a cup is set down on the sticker, the rest can reconstruct what is gone.
Almost all the machinery in Paris's font exists to produce those 56 bits. Scanning characters into bytes, filling the length field, expanding to bits — cheap. The parity, by his own design notes: 152 large contextual lookups, roughly four thousand toggle actions, a few thousand intermediate glyphs. The README even ships a build target that skips it — make fast-placeholder, "a faster layout-only build with placeholder zero parity." That font still draws a lattice of black squares. It simply doesn't scan.
Which is to say: what makes a QR code a code is the part of it that says nothing. And this typeface, measured by machinery, is almost entirely that part.
The method itself is elegant. He does not implement GF(256) polynomial division inside the font; he linearises the problem outside first. For a fixed version and error-correction level, the 56 parity bits are a linear transformation of the 152 data bits over GF(2). Feed a reference encoder 152 test vectors, each with a single bit set, record the 56 parity bits that come back, and that is column i of the matrix. Then every data bit compiles down to one contextual rule: when D37 is 1, toggle P2, P5, P8, P11, and so on.
There is a hard boundary here, and he puts it in plain sight. The QR standard asks an encoder to score eight mask patterns and pick the least ugly. Paris fixes mask 0 and bakes it into the glyph outlines, noting that automatic mask selection "would probably exceed the Reed–Solomon portion in complexity." A machine that can only substitute can compute a remainder; it cannot compare two futures. Saying that out loud is worth more than engineering around it.
The picture is made the same way. Each bit position has two glyphs, D37_0 and D37_1: one empty, one carrying a filled square at that module's coordinates, the two swapped wherever the fixed mask bit is 1. Every intermediate glyph has zero advance width, so they all stack at a single origin. A final QR_BASE glyph supplies the finder patterns, timing patterns, format bits, quiet zone, and the only nonzero width in the run. The image, in his phrase, is "the union of hundreds of tiny glyph outlines."
The order of the pipeline leaves a wound. Browsers break lines on the raw Unicode before they call the shaper, so they cannot know the text is about to become a QR code. Chrome splits at a space or a dot and shapes each run separately; the second run has no opening bracket, so it renders, honestly enough, as coded]. There is a quieter scar too: he deleted the GDEF table outright, because Firefox subpixel-snaps zero-advance mark glyphs against their nearest base glyph and the parity section drifted sideways from the data. Part of this font is defined by an omission.
QR codes are finished construct. ISO/IEC 18004, infrastructure, and "QR art" has had a name for years. Type design is finished construct. Even the fact that OpenType shaping can be made to compute is not new — Paris mentions in passing a 2026 demonstration that implemented a raycaster inside the TrueType hinting VM. The beauty of an already-construct is sediment from a chisel-construct cycle that has already closed. It is good to look at. It has stopped growing.
What has no name yet is this object. It is not a typeface — nobody will set a book in it. It is not a program — it does not execute. It is not net art — it lives in a repository with a couple of dozen commits, made by an engineer who did not bother writing an about page. (The design memo reads like the transcript of a conversation; its citation links still carry ChatGPT's tracking parameters. That does not change what the artifact is.) The art world will eventually find a word for typefaces that do arithmetic, and on that day this gets filed. It hasn't been yet.
The reason to look now is plain. The parity bits have always been in front of you. You have looked at them every time you have scanned anything — 56 of the 208 data modules in a Version 1 symbol, saying nothing at all. Making a remainder visible usually means lifting it out and setting it on a plinth. Paris did the opposite: he built a machine that runs almost entirely for the sake of the remainder, and the remainder finally showed its size.
qr.jim.sh ↗