上一篇《(三)文字排版与渲染:现代字体的对象与结构》拆开了字体家族、具体样式、字体资源、资源内部数据和文件载体。那是一张同一时刻的对象地图:它帮助我们判断当前说的“字体”究竟指哪一层。本文继续沿用这些对象,但改用时间顺序,追踪它们怎样产生、交付和被使用。
以网页使用一款字体为例,浏览器的网络与资源加载部分先根据页面声明下载 .woff2 文件,字体解析部分再从网络包装中还原可读取的字体数据。准备显示正文时,字体匹配部分从可用字体中选出实际资源,文字塑形部分再利用资源内部的字符映射和排版数据,确定实际字形与位置。绘制阶段,字体引擎和栅格器按照实际 glyph ID 读取字形描述,把轮廓或其他源图形转换成当前屏幕可以绘制的结果,绘制与合成部分最后为它着色,并把文字与页面其他内容组合起来。以 Chromium 的公开渲染架构为例,在使用 GPU 的路径中,这些栅格化、着色和合成工作会调用图形 API 与着色器;在软件路径中,相应工作也可能由 CPU 完成。不同浏览器和操作系统会组合不同实现,因此这些名称首先表示处理职责,不一定对应一组固定的软件模块。
以上是资源已经进入网页以后的使用过程。把时间线向前追溯,字体制作发生在更早的位置:设计者先维护可编辑的设计材料,再通过字体构建得到能够交付的发布资源。字体制作在发布资源形成时告一段落。
发布资源进入交付与应用阶段后,才可能被重新包装为网络格式、被子集化为覆盖范围更小的新资源,或被应用转换成专用纹理。
所以字体资源生命周期,就是指一套字体数据从设计材料进入发布形态,再经历交付、应用处理和运行时绘制的完整过程。
沿着这条链路,我们既能看见字体资源在每个阶段提供了什么,也能看见外部系统接着做了什么。当两个环境显示出不同结果时,差异也能进一步落到发布资源、交付版本、运行时选择、文字塑形或最终绘制中的某一段,而不只停留在“字体看起来不同”。
一、字体设计源与发布资源
1.1 字体设计源
制作一款字体时,字体设计师通常先在专门的字体设计软件中绘制和调整字形,而不是直接创建一份 .ttf 或 .otf 文件并在其中修改。他们会处理字形轮廓、组件、字间距、定位锚点和排版规则,也可能保存用于生成不同样式的参数关系。这些能够继续编辑、检查并作为后续生产依据的数据与规则,就是字体设计源。它可以是一份文件,也可以是一组彼此配合的材料。
按照字体设计本身主要保存在哪种材料中,可以先区分两类常见的设计源:
- 交互式设计源:设计者在字体编辑器中直接观察并修改轮廓、组件和间距,工作结果保存在编辑器项目或通用源文件中。这与视频制作很相似:Premiere Pro 或 After Effects 是创作工具,保存的项目文件是可以继续剪辑的源工程,导出的 MP4 或 AVI 才是交付结果;字体编辑软件与字体源文件,也处在发布字体之前的创作阶段。
- 程序化设计源:设计者用代码直接描述字体的设计参数和生成规则。代码可以规定整体参数、笔的形状和宽度、glyph 中各部分的几何关系、路径怎样连接,以及某些尺寸怎样随参数变化。这些源代码与它使用的参数本身就是可持续修改的设计源;执行它们以后,程序再计算出字形、度量等字体数据,并生成字体文件或把结果交给后续构建。
METAFONT 就提供了一个具体例子。设计者在 METAFONT 源代码中描述字体的整体参数、笔形、glyph 各部分的几何关系、路径连接方式和尺寸变化规则。运行这份源代码后,METAFONT 会结合目标设备参数和分辨率计算最终的点阵 glyph,通常生成 GF(Generic Font)点阵文件,同时生成供 TeX 排版使用的 TFM 度量文件;辅助工具还可以把 GF 压缩为设备驱动常用的 PK 点阵文件。在这条路线中,可编辑的源头确实是参数和程序,典型产物则是面向具体设备的点阵字体与度量数据。
程序化设计源与交互式设计源的区别,并不等于“使用代码”与“不使用代码”。在程序化路线中,代码既保存字体设计本身的参数和生成规则,也负责据此计算字形数据,并生成字体文件或把数据交给后续构建。交互式路线同样可以运行代码,但字体设计仍保存在编辑器项目中,代码只负责批量检查、导出或格式转换。这样的脚本承担了程序化路线中“处理和输出已有设计”的部分职责,却没有用代码保存字体设计本身,因此不会成为程序化设计源。
两种设计方式也不是彼此隔离的。从字体制作到形成初始产物的过程中,程序化步骤可以先生成一份可编辑的中间数据,设计者再在字体编辑器中继续调整;调整后的结果还可以交回程序化步骤继续计算、合并或输出,新的中间结果也可以再次进入编辑器。程序化步骤与交互式操作因而可以在同一条生产流程中前后衔接,甚至多次交替出现。
程序化设计源也不等于字体内部的运行时绘制指令。前者在制作阶段生成字体数据;后者例如 Type 1 CharString,已经是发布资源中的字形描述,供字体引擎在使用阶段解释。两者都可能表现为“程序”,但发生的时间、操作的对象和产生的结果都不同。
设计源保存的内容通常比一份发布字体更丰富:它可以同时包含多个字重的母版、共享组件、变化关系和生产配置。生产者可以从中选取并组织不同内容,分别输出 Regular 与 Bold 两份静态资源,也可以把变化关系保留在一份可变字体资源中;还可以针对不同语言范围、产品版本或发布渠道生成不同结果。
因此,一份发布资源可以理解为设计源在某组生产目标下形成的结果,但它不只是对源数据做一次简单切片或字节压缩。构建过程还会选择内容、计算数据、建立关联,并把结果组织成字体规范要求的结构。设计源是生产依据,发布资源才是用户运行时能够选择和读取的数据实体。
1.2 字体构建与发布资源
让设计材料变成浏览器、操作系统或应用能够读取的字体,需要经过一次字体构建。它对应视频制作中的“导出”:源工程仍然保留可编辑材料,导出过程则按照目标格式计算并组织数据,得到可以独立播放的 MP4 或 AVI。字体构建同样会读取设计源,把轮廓、映射、度量、命名和排版数据组织成目标字体规范要求的结构,执行必要的计算与检查,再输出可以独立使用的发布资源。
开源工具 fontmake 就承担这类职责:它可以读取 Glyphs、UFO 或 Designspace 等源数据,生成静态或可变的 OpenType 字体。
构建可以输出两类资源:
- 如果构建过程把粗细、宽度、倾斜等设计参数固定下来,输出的就是记录一个确定状态的静态资源;多个固定状态可以分别构建成一组静态资源。
- 如果构建结果保留设计轴、变化区间和相应的插值数据,输出的就是可变资源;应用使用它时,还要为各条轴确定坐标,才能得到当前实例。
无论静态还是可变,构建结果都已经形成自己的数据边界。一份发布资源有自己的版本、字符覆盖、数据表目录和 glyph ID 命名空间。glyph ID 只在这份资源内部指向字形:另一份资源里编号相同的 glyph ID,不保证表示同一个字符,也不保证具有相同形状。
同一套设计源经过不同构建配置,可以得到字符覆盖、排版数据、轮廓格式或版本不同的多份资源。反过来,两份文件即使显示相同的家族与样式名称,也不证明它们是同一份构建结果。例如,网站把旧版的 Example Sans Regular 换成同名新版,或者在不改变显示名称的情况下部署一个只覆盖部分字符的版本,界面里看到的名称仍然相同,实际版本、字符覆盖和内部数据却已经改变。
二、字体交付与应用派生
上一章得到的是可以独立读取的发布资源。资源要进入网页、操作系统或应用,还要有实际的存储与交付方式;应用也可能为了体积或绘制效率,继续从它生成新的资源或专用资产。本章追踪的正是发布之后、运行之前发生的这些变化。
2.1 文件载体与网络包装
发布资源离开生产环境后,最常见的落点是一个字体文件。上一篇已经区分过独立文件与字体集合,它们的对应关系是:
| 文件载体 | 通常承载什么 | 运行时怎样选择 |
|---|---|---|
.ttf/.otf | 一份 OpenType 字体资源 | 直接使用这份资源 |
.ttc/.otc | 由多份字体资源组成的集合 | 先打开集合,再选择其中一名成员 |
只有集合文件内部存在“集合成员”这层关系;普通 .ttf 或 .otf 文件通常就是一个文件对应一份资源。
.ttf 和 .otf 也可以直接作为 Web 字体传输,但字体文件越大,页面下载和首次显示的成本越高。WOFF 与 WOFF2 因此把已有字体数据重新包装和压缩,以更适合网络分发;浏览器收到后,再恢复出字体系统需要的数据。
这里可以按三个层次理解:
.ttf或.otf通常直接承载一份 OpenType 字体资源;- OpenType 在内部使用
sfnt的数据表组织方式; - WOFF 或 WOFF2 再对这种表结构数据进行网络包装。
日常使用时,说“把 TTF/OTF 包装成 WOFF/WOFF2”已经足以说明对象关系,不必从 sfnt 一词开始倒推多个层级。
WOFF1 与 WOFF2 的主要处理方式并不相同:
| 包装格式 | 接收的资源单位 | 主要处理 | 解码结果 |
|---|---|---|---|
| WOFF1 | 一份字体 | 各数据表可以分别使用 zlib 压缩 | 恢复输入字体的原始数据表 |
| WOFF2 | 一份字体或字体集合 | 先重新组织部分表中的重复数据,再把表数据放入一个 Brotli 压缩流 | 重建功能与输入等价的字体数据 |
WOFF2 所说的“变换”,不是修改字形设计,而是换一种更利于压缩的方式保存相同关系。例如,glyf、loca 和 hmtx 等表里存在可以从其他字段推导或重复出现的数据,编码器会把相关字段拆开、重新排列,并省略解码时能够重新计算的部分;浏览器再按相反规则把它们重建出来。这样可以减少冗余,让 Brotli 更容易压缩,同时保留输入字体的功能。
WOFF 1.0 规范把 WOFF 明确描述为现有字体格式的容器或包装; WOFF 2.0 规范则定义了表变换、Brotli 压缩和字体集合支持。WOFF2 解码后的字体在功能上应当与输入字体等价,但经过规范允许的重建后,二进制字节不一定与输入文件逐字节相同。
因此,.ttf/.otf 与 .woff/.woff2 不表示四种并列的字形技术。前者通常直接承载 OpenType 字体资源,后者说明同类字体数据为了便于网络传输,而选择以何种方式重新包装。一份 TTF 或 OTF 可以包装成 WOFF,也可以包装成 WOFF2;包装会改变文件中的字节组织和体积,却不改写字体原来定义的字符映射、字形与排版数据。
浏览器解包后,读取的仍然是字体原有的字符映射、字形描述、度量和排版数据。因而,这一阶段发生的变化可以准确收拢为:网络交付的文件更适合传输,解码后供字体系统使用的数据在功能上仍与包装前一致。
技术边界:WOFF1 规范允许把合规的 TrueType、OpenType 或 Open Font Format 输入包装为 WOFF,但一份 WOFF1 只承载一份字体;WOFF2 还定义了字体集合的包装方式。是否能够编码某种
sfnt数据,与浏览器是否支持其中的具体字体内容,是两个不同问题。普通 Web 场景仍以 TTF/OTF 形式的 OpenType 资源及其 WOFF/WOFF2 包装为主。
2.2 字符子集
如果一款字体覆盖数万字符,而某个网页只需要其中一小部分,直接交付完整资源会带来不必要的体积。生产或部署环节可以从原资源中选出目标字符与字形,再构建一份覆盖范围更小的资源。这个过程就是字符子集化,结果称为字体子集。
子集化并不是从文件中机械删除“暂时不用的轮廓”。目标字符经 cmap 指向基础字形后,复合字形可能还需要依赖其他组件,排版规则也可能把输入字形替换为另一组字形。可靠的子集工具需要沿这些依赖扩展保留集合,再重写字符映射、字形编号和相关数据,使新资源内部仍然自洽。fontTools Subsetter 的说明就把字符、glyph ID、OpenType 排版特性及其闭包处理列为子集操作的一部分。
“新字体资源”是这里的关键。子集化直接改变的是资源保留的数据:原资源中的某个 glyph ID 在子集里可能被重新编号,也可能不再存在。可靠的子集工具会沿依赖保留目标文本需要的组件、替换字形和定位数据;如果生产配置主动删掉某些排版表或功能,相应行为才会随数据一起消失。无论是否保留原来的显示名称,完整版与子集版都不能仅凭名称视为同一份数据。
字体子集同样是一份内部数据能够自洽的独立字体资源,因此,它和完整版资源一样,可以继续被包装成 WOFF 或 WOFF2。两步改变的对象不同:子集化重新选择并组织资源内容,WOFF/WOFF2 再改变这份资源的交付字节和传输体积。
这一区别会直接出现在可观察结果中:同一字符在本机安装的完整版里能够显示,在网页上却变成缺字标记,就需要检查网页实际下载的子集是否包含该字符。如果页面按字符范围请求多份子集,还要确认目标文本最终取得了哪一份资源。具体怎样选择字符集合和拆分请求,属于部署策略,不在本文展开。
2.3 字形图集与距离纹理的生成
应用取得字体资源后,还可以为自己的绘制管线提前准备派生资产。游戏、地图或高频界面不必在每一次绘制中都重新读取轮廓并生成同一枚字形的图像,而可以选择把已经生成的结果保存起来复用。上一篇已经介绍过字形图集与 SDF、MSDF 的对象边界;这里继续追踪它们怎样生成。
生成前,应用至少要确定一份确切字体资源、要处理的字形集合,以及当前管线需要的尺寸或缩放范围。输入不同,生成出的纹理内容也会不同。
两条常见的生成路线是:
- 位图图集:应用先按目标尺寸把每枚字形生成一张小位图,再把许多小位图排进一张或少量几张大纹理。与此同时,它建立“字体资源与 glyph ID → 纹理区域”的查找记录。纹理图像通常会上传到图形内存,区域坐标和其他查找信息则由应用保存在内存或绘制缓冲中;具体存放位置由图形架构决定。
- SDF/MSDF 图集:生成器不保存普通字形截图,而是从字形源形状计算各位置到边界的距离,再把单通道或多通道距离数据排进纹理。运行时着色器采样这些值,在目标尺寸上判断边界内外与边缘透明度。上一篇对 SDF/MSDF 的解释已经说明两类距离纹理分别保存什么。
这些派生工作既可以在应用构建阶段提前完成,也可以在运行时按需发生。动态生成通常遵循一条短流程:
- 应用第一次遇到图集中尚不存在的 glyph ID;
- 它从确切字体资源读取相应字形描述,生成位图或距离纹理区域;
- 新区域被放入现有图集或新建图集,同时记录 glyph ID 与纹理坐标的对应关系;
- 后续再次绘制相同条件下的字形时,应用直接复用该区域。
无论生成发生在何时,输出都只服务于当前应用和绘制条件:它通常不再携带完整 cmap、家族命名和全部排版数据,也不能作为通用字体资源被其他软件安装和选择。应用派生延长了资源的使用链路,却仍然是可以从原字体资源重新生成的专用结果。
三、运行时的资源选择
字体资源完成交付或应用准备后,才真正进入某一段文字的运行过程。此时,页面只有“想使用哪个家族、字重和样式”的请求,系统还需要把这个请求落实到一份能够提供字形数据的确切资源。
3.1 确定字体资源
当应用准备显示一段文字时,字体系统会根据家族、字重、样式和字宽等条件匹配可用字体。上一篇已经说明静态资源、集合成员和可变实例之间的对象关系;在当前流程里,只需要保留这一步的结果:系统选出确切资源,必要时再确定集合成员和可变轴坐标。
如果首选资源不包含某个字符,系统会继续沿 fallback 字体 的候选顺序查找。候选可以来自 CSS 或应用明确给出的字体列表,也可以来自平台自己的匹配规则;系统默认字体或最后保底字体可能位于这条链路中,但不是所有场景都会先选它。一行文字因此可能由多份资源共同提供。
资源选择必须与后续字形编号一起保留,原因很具体:glyph ID 是资源内部编号。同样是 glyph ID 42,放在首选资源和兜底资源中可能指向完全不同的字形。系统只有同时知道“哪份资源的哪个 glyph ID”,才能继续读取正确的字形数据。
不同浏览器、操作系统和应用怎样排列候选、检查覆盖并完成 fallback,属于更完整的字体匹配策略。当前只需确认一个不随实现变化的关系:每次字符映射都针对一份确切资源,得到的 glyph ID 也只在这份资源中有效。
3.2 基础字符映射
找到确切字体资源后,系统便可以在这份资源内部查找字符。以拉丁大写字母 A 为例,文本中保存的是码点 U+0041;字体资源的 cmap 数据把这个码点映射到当前资源中的一个 glyph ID。这个编号指向该资源中的一枚字形,是字符与字体内部数据建立联系的第一步。OpenType cmap 规范将它定义为字符编码到 glyph index 的映射。
这里得到的是基础字形:它只表示 cmap 为当前字符提供的直接映射,还不等于最终用于绘制的字形。相邻字符、书写系统、语言和排版功能都可能在后续处理中改变字形选择或位置。对于简单拉丁文本,基础字形与最终字形经常相同;遇到连字、上下文变体或组合定位时,两者便可能不同。
在 OpenType 资源中,glyph ID 0 保留为“未定义字形”的位置,通常称为 .notdef。这个角色约定对字体资源是共通的,但 .notdef 的具体外观并不共用:每份资源都可以提供自己的缺字图形。当 cmap 找不到目标字符时,资源内查询会得到这个默认编号;更高层系统也可能改用其他 fallback 字体,最终是否显示缺字标记仍由外部匹配策略决定。
基础映射解决的是“这个字符在当前资源里直接对应哪枚字形”。到这里,每个字符已经有了资源内部的基础 glyph ID,但字符上下文造成的替换与位置变化尚未执行。
四、文字塑形与字形序列
第一篇对高级排版的介绍已经区分了字形替换与字形定位:选出基础字形以后,系统还可能根据上下文换用另一枚字形,或者保留字形身份、只调整它的位置。实际执行这两类处理的过程就是文字塑形(text shaping);塑形得到的有序运行时结果,则是本章所说的字形序列。
4.1 字形选择与定位
塑形引擎是浏览器、操作系统或应用文字系统中的软件模块。它接收字符序列与确切字体资源,并结合书写系统、语言、文本方向和启用的排版功能,输出实际采用的 glyph ID 及其位置。HarfBuzz 是常见的开源文字塑形引擎;平台或应用也可以通过自己的文字 API 提供同类职责。
在英文术语里,书写系统被称为
script,但是这里的script是指拉丁文、阿拉伯文、天城文等字符在书写结构上的类别,不是编程脚本。
前两篇已经说明,OpenType 资源可以用 GSUB 提供字形替换数据,用 GPOS 提供字形定位数据。这里不再重复两张表的定义,而是看塑形引擎怎样把它们用于当前文本:
- 取得基础字形:对
office逐字查询cmap,先得到o、f、f、i、c、e对应的基础 glyph ID。 - 执行字形替换:如果当前资源含有
ffi连字,当前书写系统和功能设置也允许使用,塑形引擎便按 GSUB 数据把连续的三个基础 glyph ID 替换成一个ffi连字 glyph ID。这里改变了实际选中的字形。 - 执行字形定位:对于
To这样的字形对,塑形引擎可以按 GPOS 数据缩小两者之间的推进距离;组合附加符号时,也可以调整局部偏移。这里可以不更换 glyph ID,只改变字形怎样放置。
资源提供可用的字形与规则,塑形引擎读取当前上下文并执行规则。引擎不会凭空生成资源中不存在的连字,字体数据也不会离开外部引擎自行处理当前文本。
因此,“字形选择与定位”不是静态查看两张数据表,而是一次明确的运行时动作:先从基础字形出发,再由塑形引擎根据当前条件执行适用的替换与定位,得到本次真正要使用的字形及其相对位置。不同书写系统的完整规则不在这里展开。
4.2 带位置的字形序列
塑形完成后,输出是一组带位置的字形序列。“序列”表示这些字形按照本次排版使用的次序组成一个有序列表;列表中的每一项代表一枚实际字形,并记录它的 glyph ID、推进与偏移等位置数据,以及它与原始文本范围的关联。
继续看 office。假设当前字体与功能设置允许 ffi 连字,塑形前会有 [o]、[f]、[f]、[i]、[c]、[e] 六枚基础字形,塑形后则输出 [o]、[ffi]、[c]、[e] 四项。
注意,这里的 [ffi] 指代的是实际连字对应的 glyph ID。它在序列中只占一项,却共同表示原始字符串中的 f、f、i 三个字符。
一项字形记录中的几类信息各自承担不同职责:
| 信息 | 在 office 示例中的作用 |
|---|---|
| 实际 glyph ID | 决定稍后从资源中读取 o、ffi、c 或 e 的哪份字形描述 |
| advance | 决定画完当前字形后,绘制位置沿排版方向前进多少 |
| offset | 决定当前字形相对基准位置向水平或垂直方向移动多少 |
| 文本—字形对应(cluster) | 让系统知道 [ffi] 这枚输出字形共同表示输入位置 1—3 的 f、f、i |
这里的 cluster(塑形关联组),首先是塑形过程中必须作为一个整体跟踪的一段输入字符。所谓“它与原始文本关联”,具体是记录:输入字符串中的哪一段字符,由塑形结果中的哪一枚或哪几枚字形共同表示。这种对应不是逐字符、逐字形的一一配对。在 office 中,三个输入字符 f、f、i 合成一枚 [ffi],因此这三个字符和这枚输出字形属于同一个 cluster;反过来,如果一个输入字符拆成多枚输出字形,那几枚字形也可以属于同一个 cluster。
HarfBuzz 的 cluster 机制用一个整数值保存这层对应关系的线索。这个值由调用方随输入字符提供,本身可以任意指定,实际使用中通常取该字符在输入文本中的位置。以这里的 ASCII 字符串为例,可以把 office 六个字符的输入位置依次标为 0、1、2、3、4、5。如果调用方按照输入顺序提供这些递增的位置值,HarfBuzz 在常用模式下会让输出值继续保持这个顺序;形成连字后,[o]、[ffi]、[c]、[e] 四枚输出字形的 cluster 值可以依次为 0、1、4、5。
此时,[ffi] 的 cluster 值 1 不是完整的文本范围,而是这组字符在输入文本中的起始位置标记;下一组从位置 4 开始,所以上层系统可以在这个从左到右的例子中推断出 [ffi] 对应 [1, 4),也就是位置 1、2、3 上的 f、f、i。HarfBuzz 的塑形输出会保存每枚输出字形的 glyph ID 与这个 cluster 值,并另行保存 advance 和 offset。面对其他文本方向或更复杂的塑形结果,上层系统仍要结合相邻 cluster、文本方向和原始字符串确定完整范围;光标与选区的精细行为,也要在这层对应关系之上继续处理。
字形数量因此不必等于字符数量:多个字符可以合成一个连字字形,一个字符也可能需要多枚字形共同显示。下游绘制必须使用塑形后的有序列表,不能丢掉其中的实际 glyph ID 和位置,再退回原始字符逐个排列。
完成这一步时,文字还没有进入栅格化与最终绘制。带位置的字形序列只确定三件事:按什么顺序使用哪些实际字形、每项怎样推进和偏移、每组输出字形共同表示输入文本中的哪段字符。这就是字形序列在当前生命周期中的完整边界。
五、字形绘制接口
上一章的输出已经确定“使用哪些实际字形,以及这些字形放在哪里”。绘制阶段接收这份带位置的字形序列,再结合序列所属的字体资源与其中的 glyph ID,找到真正需要成像的字形数据。
5.1 字形描述与当前尺寸
字形序列中的两类信息会在这里分开发挥作用:
- 实际 glyph ID 决定画什么。如果塑形发生了字形替换,序列中保存的是替换后的 glyph ID,字体引擎会据此读取连字或上下文变体的字形描述;
- advance 与 offset 决定画在哪里。如果塑形只调整位置(字形定位),glyph ID 仍指向原来的字形描述,绘制时再应用新的推进与偏移。
glyph ID 本身只是当前资源内部的索引。字体引擎要用“字体资源 + 实际 glyph ID”读取对应的 TrueType 轮廓或 CFF/CFF2 CharString、内置位图或彩色字形数据,才能取得本次要形成的图形。
字体引擎取得字形描述后,会按当前字号和变换把轮廓调整到本次使用的尺寸上;在需要时,还会应用 hinting 或网格适配,让关键笔画更合适地落在设备网格上。这一步的结果可以是已经缩放的轮廓,也可以是从字体资源中选出的内置位图或彩色图形内容。
到这里,字形描述、glyph ID 和位置之间的关系已经闭合:资源保存可供读取的源描述,塑形结果指定本次实际采用的编号与位置,字体引擎再把两者连接起来,准备当前输出条件下的字形图形。
本文所说的四种“引擎”或“器”,首先是业内常见的抽象职责模块:
| 抽象模块 | 当前职责 |
|---|---|
| 塑形引擎 | 决定实际字形、顺序、推进与偏移 |
| 字体引擎 | 解析资源,并读取、缩放或准备字形数据 |
| 栅格器 | 把矢量形状转换成像素网格上的覆盖结果 |
| 渲染器 | 应用颜色、透明度与图层关系,把结果合成到画面 |
不同浏览器、操作系统和应用可以自行组织这些职责:一个库可能同时承担多项工作,某些步骤也可能交给图形系统或 GPU。因而,这张表描述的是通用分工,不要求实际系统恰好存在四个独立软件包,也不规定它们只能按照一种调用方式组合。
5.2 栅格化、绘制与运行时缓存
对于轮廓字形,栅格化位于“准备好当前尺寸的矢量轮廓”与“为像素着色并合成”之间。它的输入是已经缩放到当前输出条件的轮廓,输出是一张覆盖位图:
- 完全位于字形内部的像素具有完整覆盖;
- 被轮廓边缘穿过的像素只被部分覆盖;
- 位于字形外部的像素覆盖为零。
这些覆盖值可以作为后续着色时的透明度或混合依据,使斜线和曲线边缘不会只剩生硬的整格开关。
FreeType 就是一个开源字体引擎库:它能够读取和缩放多种字形数据,也提供把轮廓转换为像素或子像素覆盖位图的栅格化能力。
但并非所有字形来源都会经过同一种“轮廓栅格化”:
| 字形来源 | 当前阶段怎样处理 | 是否经过传统轮廓栅格化 |
|---|---|---|
| TrueType 或 CFF/CFF2 轮廓 | 缩放轮廓,再计算像素覆盖 | 是 |
| 字体内置点阵或彩色点阵 | 选择匹配的位图,解码并在需要时缩放 | 否;像素已经存在 |
| 分层彩色轮廓或 SVG 字形 | 分别解释矢量图层,再由相应图形绘制路径转换成像素 | 仍需要矢量到像素的过程,但未必由同一个字体轮廓栅格器完成 |
| 预先生成的 SDF/MSDF | 着色器采样距离纹理,并把距离值转换成边缘覆盖 | 显示时不再栅格化原始轮廓;距离纹理已经在更早阶段生成 |
无论走哪条路线,渲染都还要继续:系统为栅格化产物(可能是覆盖位图,也可能是内部已经像素化的结果)应用文字颜色、透明度、混合方式和图层关系,再把它合成到页面、画布或界面中。栅格化回答矢量形状怎样落入像素网格,渲染则回答这些结果最终以什么颜色和层叠关系进入画面。
为了避免反复处理相同结果,系统可以缓存已经准备好的字形图像。缓存键至少要区分字体资源、glyph ID 和会改变像素结果的绘制条件;否则,另一份资源里的同编号字形或同一字形的另一尺寸就可能误用当前缓存。
前面介绍的图集就是一种面向批量复用的组织方式。而 SDF 或 MSDF 管线会根据实际 glyph ID 找到对应的距离纹理区域,然后着色器会在目标位置重建边缘:它把“当前位置到字形边界的距离”与设定阈值比较,并在阈值附近做平滑过渡,由此判断哪些像素属于字形内部、外部或边缘。这里所谓“重建边缘”,就是把距离数据重新转换成当前尺寸下的像素覆盖,而不是重新创造一条字体轮廓。
普通轮廓、内置位图、彩色字形和距离纹理采用的成像路线不同,抗锯齿、GPU 采样与平台绘制策略也可能继续改变边缘结果。它们在生命周期中的共同关系是:最终渲染结果从字体资源内部的数据出发,经过字形准备、成像、着色和合成等外部处理后形成。
六、总结与边界
前五章已经从设计源一路走到页面像素。最后把这些阶段重新放回同一张生命周期地图,并用实际可观察的差异检查每一段分别影响什么。
6.1 字体资源的完整生命周期
回到开篇的网站:字体设计师先完成可编辑源,生产工具把它构建成发布资源;网站可以进一步生成字符子集,再用 WOFF2 包装并部署。浏览器下载和解包资源后,按照页面样式与字符覆盖选出本次实际使用的字体,用 cmap 得到基础字形,经塑形形成带位置的实际字形序列,最后读取字形描述并走完相应成像与渲染路线。完整链路可以收拢为:
这条链可以按对象变化分成四段:
- 设计与生产:交互式或程序化设计源经过字体构建,形成可被一般字体系统读取的发布资源;
- 交付与应用准备:文件或网络包装负责承载资源,子集化生成内容更小的新资源,图集与距离纹理则是应用专用派生结果;
- 运行时选择与塑形:系统先确定确切字体资源,再由字符映射得到基础字形,并结合上下文形成带位置的实际字形序列;
- 字形成像与渲染:字体引擎按实际 glyph ID 读取字形描述,轮廓、点阵、彩色图形或距离纹理沿各自路线变成能够着色和合成的结果。
这四段共同说明了一条连续关系:字体资源提供可以被读取的字符映射、字形描述、度量和排版数据;外部系统则根据当前文本与环境选择、解释并执行这些数据,逐步得到实际字形、位置和最终画面。
6.2 字体差异的定位
生命周期最直接的用途,是从屏幕上的具体现象倒查差异发生在哪一段:
| 可观察现象 | 优先检查的位置 | 需要确认的对象 |
|---|---|---|
| 同一字符在一个环境中正常显示,在另一个环境中变成缺字标记 | 资源选择与交付 | 是否加载了同一资源或子集、目标字符是否被覆盖、fallback 是否选择了其他资源 |
| 字符都能显示,但字母外形、数字样式或 emoji 图形不同 | 确切字体资源与实际 glyph ID | 版本、集合成员、可变实例或 fallback 资源是否不同,塑形是否替换了字形 |
| 字形外形相同,但字距、连字或附加符号位置不同 | 文字塑形 | 启用的排版功能、语言和书写系统条件,以及塑形引擎执行结果是否一致 |
| 字形和位置一致,但边缘深浅、平滑程度或颜色不同 | 成像与渲染 | 栅格化、内置点阵选择、抗锯齿、GPU 采样、着色和合成条件是否不同 |
检查顺序不是绝对的调用顺序,而是一条缩小范围的路径。先确认实际读取的数据,再比较塑形得到的字形与位置,最后比较成像和渲染条件,视觉差异就能逐步落到可复查的技术对象上。
6.3 字体资源全貌总结
到这里,字体资源这一层已经形成一张完整地图:数字字体怎样成为一份能够进入现代排版系统的数据资源,这份资源由哪些对象构成,又怎样参与一次真实的文字显示。三篇文章分别从形成、结构和使用三个方向完成了这张地图:
- 《(二)文字排版与渲染:数字字体的发展历程》沿技术演化说明字形怎样从实体形状变成点阵、轮廓和结构化字体数据,以及 Type 1、TrueType 与 OpenType 分别改变了什么;
- 《(三)文字排版与渲染:现代字体的对象与结构》建立当前对象地图,区分 family、face、字体资源、资源内部数据、文件载体和应用派生结果;
- 本文把这些对象放回时间,追踪设计源怎样变成发布资源,资源怎样交付、选择、映射、塑形并最终进入绘制。
下一篇会把视线转向字体资源中的空间数值:这些数值分别描述什么,系统又怎样结合当前字号,把它们转换成页面中的长度与位置。