Typst 是一个 2023 年初开源的一个排版软件。类似于 LaTeX,它通过纯文本编写源代码,然后通过编译器将源代码转换为排版好的 PDF 文件。虽然目前 Typst 的生态还不如 LaTeX,但是比起 LaTeX,它有一些明显的优势:

  • Typst 支持高效的增量编译,可以实现媲美 Markdown 的实时渲染更新。
  • Typst 被设计为一种函数式编程语言,支持变量作用域,高阶函数等现代特性。这使得编程和调试比 LaTeX 轻松很多。
  • Typst 被构建为单个可执行文件,其包管理器也开箱即用1。使用上手几乎没有障碍。

Typst 原生支持 Unicode,但是由于其作者不懂中文,0.1 版本的 Typst 对中文排版的支持非常差。笔者于 2023 年的四到五月间提交了一系列 PR,使得 Typst 的中文支持初步达到了可用的水平。本文是对实现功能及思路的简要介绍。为了在软件中实现中文排版,我们首先需要知道中文排版的规则,再将软件中缺失的部分补齐。

古典活字印刷的原则

早期的中文排版使用的是活字印刷技术。传统的活字书籍文本中没有标点,方块字紧密地纵横排列在矩形版心之中。今天中文排版的密排原则,正是源自于活字印刷。

清乾隆年间木活字《武英殿聚珍版从书》书页

排版原则:汉字密排

汉字依行排列文字,原则上文字外框彼此紧贴配置,称作密排

在方块字上面做密排带来了两个效果。一方面我们自然获得了两端对齐,另一方面一行的长度正好是汉字宽度的整数倍。这两个特征作为中文排版的基本规则被沿袭了下来。

排版原则:两端对齐

也称为头尾对齐。

排版原则:行长是字号的整数倍

Typst 中的实现

原始版本

我们来看一下 Typst 的中文排版能不能实现上面提到的三条原则。密排,用西文排版的术语来考虑就是字符间距为 0,这是默认的配置。而两端对齐和行长都可以通过 #set 规则来设置。使用 0.1.0 之前的 Typst,我们尝试对样例进行排版,规定每行排17个字。排出来的效果如下:

bad.png

需要注意的是,这是 Typst 0.1 版本,这个时候的排版代码完全是由不懂中文的原作者写的。这个排版效果已经出人意料地好了。唯一的问题是,第一行多出了两个空格,两端对齐的原则被违反了。这是因为引入了西式标点的现代中文需要遵守一些禁止规则,我们称为标点禁则

排版原则:标点禁则

点号,右夹注符号不能出现在行头;左夹注符号不能出现在行尾2

如果我们机械地按照一行 17 个字来进行排版,那么将得到以下效果:

孔雀最早见于《山海经》的《海内经》
:「有孔雀」。东汉杨孚著《异物志》
记载……

这将使得冒号被错误地放在一行的开头。实际上,如果每个字符的宽度都确定了,那么标点禁则和两端对齐的原则没有办法同时得到满足。

标点禁则是一个在全球各种语言都存在的现象,Unicode 专门有规范附件 UAX #14 规定了断行算法,以确保各语言的标点禁则得到了遵守。Typst 的作者虽然不懂中文,但是他们在 Typst 中实现了 UAX #14,于是标点禁则被自动地支持了。这就导致了两端不对齐的排版。

挤进推出

应该如何做出同时满足两端对齐和标点禁则的排版方式呢?中文排版中有「挤进推出」的方法,也就是通过挤压的方式把从下一行的字「挤进」本行,或者通过拉伸的方式将本行最后一个字「推出」到下一行。日本开发者 Alex Sayers 就通过一个 PR 实现了这种排版3。下图即合并了 PR #542 之后的排版效果。注意到下面的段落中第一行被挤进了 18 个字,而第三行被拉成了只有 16 个字。

justify.png

遗憾的是,这个排版存在着严重的缺陷:

  • 汉字自然排列时已经是密排了,如果再压缩将会使笔画交叉,造成非常不良的排版。事实上, 原则上汉字之间可以拉伸,但不能压缩
  • 采用了拉伸调整的第三行,我们发现其行尾「足」字还是没有和边框对齐。当然这只是实现上的 bug,这个调整本身是没有问题的。

正确的推出实现

为了解决这个问题,笔者实现了 PR #701。其实现的效果如下。由于改进了 Typst 内部用于断行的动态规划算法,这个版本的断行和之前的不太一样。注意到第一行被拉伸到只有 16 个字之后,其他行的字符都成功放到了纵横网格之中。

adjustment-1.png

这样一来算是成功实现了满足两端对齐和标点禁则的排版。而字间距的略微扩大,作为一个代价,也相对易于接受。不过实际上在实现标点挤压之后,大多数排版并不需要强行拉伸字间距。

标点挤压

到刚才为止,我们的中文段落排版只有一种段落调整的方式,那就是均匀调整段落中每一个字符之间的间距,来实现「挤进推出」的效果。进一步地,由于字符之间的间距默认已经是 0 了,我们为了避免不良排版,只用了「推出」的方式,即均匀地在一行中所有相邻的两个字中间,插入等量的空白。

实际上中文排版中是可以「挤进」而保持良好排版的,那就是不调整汉字,而是使用标点的调整空间来进行调整。

W3C 文档《中文排版需求》中关于标点调整空间的描述

笔者的 PR #836 引入了标点挤压。我们发现这样能使得段落变得更加紧凑,整个文本排进了四行之中。而美中不足在于行头行尾的标点有大量空白,造成了段落两边参差不齐的观感。这个问题在下一个 PR 中得到了修复。

adjustment-2.png

更多的标点挤压

在之前的实现中,标点挤压是作为一种解决标点禁则问题的手段,「挤进推出」中的「挤进」来实现的。实际上标点挤压还有一个功能是解决标点空白过大的问题。例如冒号后面跟一个左引号,如果不进行挤压,将会产生超过一个汉字宽的空白,这样段落显得松散。所以即使没有标点禁则的问题,我们往往也会对标点进行挤压。

事实上,标点挤压主要有两个基本规则,我们称为默认挤压

  • 连续的两个标点,原则上压缩掉半个字宽的空间。
  • 行头与行末的标点,原则上压缩掉头(尾)的半个字宽的空白。

在默认挤压的基础上,我们再对挤压量进行微调,以解决标点禁则的问题。PR #954 实现了这样的调整。

compress.png

最终我们得到了较为满意的排版。这个排版同时满足了两端对齐,文字密排和标点禁则。而且排布紧凑,灰度均匀。实际上,使用专业的中文排版软件 Adobe InDesign 进行排版,采用推荐的配置4,达到的效果和 Typst 相似。当然排版没有标准答案,中文排版中也存在着多种风格,InDesign 有数量繁多的配置项,可以制作出各种风格的排版。由于笔者时间有限,在 Typst 的实现中没有任何配置项。许多排版风格,例如开明式标点,标点悬挂,孤字控制等,都没有实现。换言之 Typst 的中文排版目前提供的是一个「基本正确」的默认风格实现。不过这对于多数普通用户而言已经够用了。

1

截止到本文完成时,Typst 的包管理功能还处于预览状态。

2

这实际上是一个近似的说法,详细的规则见 https://w3c.github.io/clreq/#prohibition_rules_for_line_start_end

3

日文中同时有汉字,平假名和片假名三种书写系统,它们都是方块字,且采取密排原则。由于日文和中文的历史渊源,两种语言的排版原则存在大量相同之处。本文中提到的几乎所有排版问题都同时适用于两种语言。

4

即「简体中文避头尾」+「所有行尾 1/2 个字宽」+「Adobe CJK 单行书写器」+「两端对齐」+「避头尾间断类型:确定调整量优先级」。详见 The Type 的相关文章