1. 介绍
本节为非规范性内容。
增量字体传输(IFT)是一种用于改善 Web 上远程字体(或称“Web 字体”)延迟的技术。 如果没有这项技术,浏览器必须下载字体的每一个字节,才能使用该字体渲染 任何字符。IFT 允许浏览器仅下载文件中的部分字节, 从而 缩短从浏览器意识到需要某种字体,到能够使用该字体渲染 所需文本之间的感知延迟。与传统字体子集化方法不同,增量 字体传输 会保留各片段之间布局规则的编码(渐进式字体扩充:评估报告 § fail-subset)。
WebFonts 的成功应用并不均衡。本规范使 WebFonts 能够用于 目前因网络缓慢、字体非常庞大或子集化要求复杂而无法使用的场景。例如,即使使用 WOFF 2 [WOFF2],中日韩语言的字体仍可能大到不切实际。
1.1. 技术动机:评估报告
有关促成本规范形成的研究,请参阅《渐进式字体扩充:评估报告》[PFE-report]。
1.2. 概述
增量字体是 一种常规的 OpenType 字体,它经过重新格式化以包含增量功能, 其中部分功能由两个额外的表提供。使用 这些新表,可以通过加载补丁并将其应用到字体来扩充字体 (例如覆盖更多码位)。
IFT 技术由四个主要部分组成:
-
§ 4 扩展字体子集:提供客户端用于 选择和应用补丁的算法。
-
§ 5 字体格式扩展:定义新表,其中 包含可应用于字体的补丁列表。
-
§ 6 字体补丁格式:定义可使用的两种不同补丁类型。 一种是“通用”二进制补丁,另一种 专用于字体存储字形数据的格式。
-
§ 7 编码:创建构成增量字体的字体及其相关 补丁。
从高层次来看,增量 字体的使用方式如下:
-
客户端根据要渲染的内容选择、下载并 应用补丁,以扩展 字体,使其覆盖更多字符、布局特性和/或变体空间。每次出现新 内容时都会重复此步骤。
1.3. 创建增量字体
预计生成增量字体最常见的方式,是将现有字体转换为使用本规范定义的 增量编码。从高层次来看,将现有字体转换为增量字体的过程 如下:
-
选择初始子集的内容,该内容将包含在客户端加载的 初始字体文件中, 通常包括原始字体中预计始终需要的任何数据。
-
选择一种或多种补丁类型。不同补丁类型具有不同特性,因此不同使用 场景可能需要不同的 补丁类型,某些情况下也可能混合使用两种类型。
-
选择字体的分段方式。客户端将使用补丁把各个片段添加到基础子集中。 选择 合适的分段方式,是生成高效编码过程中最重要的部分之一。
-
根据这些选择生成一组补丁,其中每个补丁都相对于 初始字体文件或之前的补丁, 添加特定片段的数据。
-
生成包含初始子集和补丁映射的初始字体文件。该映射 列出所有可用补丁文件、它们所在的 URL,以及补丁将向字体 添加哪些数据的信息。
注:这是对 创建增量字体过程的高度简化描述;有关生成编码以及 编码要求的更深入讨论,请参阅 § 7 编码一节。
1.4. 性能注意事项与增量字体传输的使用
是否适合使用增量传输,取决于字体、 网络和被渲染内容的特性,因此它并非始终有益。本节提供非规范性指导,帮助确定 何时应使用增量传输。
增量字体通常会触发并行加载多个补丁。因此,为最大限度 提升性能,在 提供增量字体时,建议使用支持多路复用的 HTTP 服务器(例如 [rfc9113] 或 [rfc9114])。
与加载整个字体相比,增量加载字体存在根本性的性能权衡。 简单来说,增量传输可能传输更少的字节,但潜在代价是 增加所发出的网络请求总数和/或增加请求处理 延迟。通常,只有当减少传输字节所降低的延迟 超过增量传输方法引入的额外延迟时,增量字体传输才有益。
首先需要考虑的是被渲染内容的语言。评估报告 包含对三类语言进行增量字体传输模拟的结果 (渐进式字体扩充:评估报告 § langtype)。有关 增量字体传输在各语言类别中的预期性能,请参阅其结论部分 渐进式字体扩充:评估报告 § conclusions。
其次,预计需要字体中的多少内容?如果预计渲染内容时需要 字体的大部分内容,那么增量字体传输不太可能带来收益。但在许多情况下, 预计只需要字体的一部分。例如:
-
字体支持多种语言,但预计用户仅渲染 其中一部分语言的内容。
-
被渲染内容只使用字体中全部字符的一小部分。这种情况 常见于中文、日文、韩文、Emoji 和图标字体。
-
只渲染少量文本。例如,仅用于 标题的字体。
增量传输的一种替代方案,是将字体拆分为不同子集(通常按文字系统拆分), 并使用 @font-face 的 unicode range 特性仅加载需要的子集。但是,如果不同 子集中的字符之间存在布局规则,这可能改变某些内容的渲染结果,参见 渐进式字体 扩充:评估报告 § fail-subset。增量字体传输不会遇到此问题,因为它可以涵盖 原始字体及其全部布局规则。
1.4.1. 减少网络请求数量
如上一节所述,最基本的增量字体传输实现 与传统字体加载相比,往往会增加请求总数。由于每次扩充 通常至少需要一个往返时间,如果发出过多 请求,性能可能会受到负面影响。根据可用的补丁类型以及补丁映射中提供的信息量,客户端可以预先请求当前 尚不需要、但预计未来会需要的码位补丁。实现 对此特性的智能使用可帮助减少请求总数。评估报告通过 测试一种基于基本字符频率的码位预测方案来研究这一点,并发现其 改善了整体性能。
2. 选择启用机制
Web 页面可通过在 ''@font-face'' 块中使用 CSS 字体技术
关键字(CSS 字体 4 § 11.1
字体技术),选择为某种字体启用增量传输。关键字 incremental 用于表明
所引用字体包含 IFT 数据,并且
仅应由支持增量字体传输的用户代理加载。
@font-face {
font-family: "MyCoolWebFont";
src: url("MyCoolWebFont-Incremental.otf") tech(incremental);
}
@font-face {
font-family: "MyCoolWebFont";
src: url("MyCoolWebFont-Incremental.otf") tech(incremental);
unicode-range: U+0000-00FF;
}
如第二个示例所示,unicode-range 可与 IFT 字体结合使用。 Unicode 范围应设置为与完全扩展字体的覆盖范围相匹配。这样,当字体不支持 任何所需码位时,客户端便可 避免尝试加载 IFT 字体。
另一种方法是使用 CSS supports 机制,它可以根据 字体技术进行选择:
@when font-tech(incremental) {
@font-face {
font-family: "MyCoolWebFont";
src: url("MyCoolWebFont-Incremental.otf");
}
}
@else {
@font-face {
font-family: "MyCoolWebFont";
src: url("MyCoolWebFont.otf");
}
}
注:每个单独的 @font-face 块可以
选择启用或不启用 IFT。这是因为
字体在 Web 页面上的使用方式多种多样。作者可以控制希望对哪些字体使用
此技术,以及不对哪些字体使用。
注:IFT 技术关键字可以与其他
字体技术说明符结合使用,以执行
字体特性选择。例如,@font-face 可以包含两个 URL,一个使用
tech(incremental, color-COLRv1),另一个使用
tech(incremental, color-COLRv0)。
2.1. 离线使用
在某些情况下,用户代理可能希望保存 Web 页面以供离线使用。保存的页面可能在没有 网络连接时查看, 因而无法请求增量字体引用的任何额外补丁。由于内容发生变化时(例如由于 JavaScript 执行) 无法扩展 增量字体,因此页面保存机制应通过调用 完全扩展字体子集来 完全扩展增量字体,并将对增量字体的引用替换为完全扩展后的字体。
3. 定义
3.1. 字体子集
字体子集是字体文件 [iso14496-22] 的修改版本,其中仅包含 渲染以下内容子集所需的数据:
这些内容均由原始字体支持。当使用正确构造的字体子集,通过任意 组合的子集码位、布局 特性或设计变体 空间渲染文本时,其渲染结果与原始字体完全相同。这包括使用渲染器可能选择使用的原始字体中的任何 可选排版 特性进行渲染,例如提示指令。设计 变体空间使用用户轴标度指定(OpenType 规范 § otvaroverview#coordinate-scales-and-normalization)。
字体子集 定义描述字体 子集所支持的最少数据(码位、布局特性、 变体轴空间)。
注:为方便起见,本文档其余部分 链接到 [open-type] 规范,它是 [iso14496-22] 的副本。
3.2. 字体补丁
字体补丁是一个 对要应用于 IFT 编码字体的更改进行编码的文件。补丁用于扩展 现有字体子集并 提供更广的覆盖范围。
补丁格式是 相对于字体子集应用的更改的指定编码。按照该格式编码的一组 更改就是字体补丁。每种补丁格式都有一个关联的补丁应用算法, 该算法以字体子集和一个使用补丁格式编码的字体补丁作为输入, 并输出扩展后的字体子集。
3.3. 补丁映射
补丁映射是一个OpenType 表,它对一组 从字体子集定义到承载补丁的 URL 的映射进行编码,这些补丁用于 扩展增量 字体。补丁映射表编码 一组补丁映射 条目,其中每个 条目都有键和值。条目的键定义条目的覆盖范围,即关于 哪些子集定义将与其匹配的信息。值指定在条目 匹配时应应用哪些补丁的信息。补丁映射 条目摘要:
| 键 | 值 |
|---|---|
有关补丁映射格式的更多详细信息,请参阅 § 5 字体格式 扩展。
3.4. 数据类型说明
本规范其余部分中的编码数据结构,使用 OpenType 规范 § otff#data-types 中定义的数据类型描述。与 OpenType 的其余部分一样,所有字段均使用“大端序”字节 顺序。
4. 扩展字体子集
本节定义客户端用于扩展增量字体子集,以覆盖更多 码位、布局特性和/或设计空间的算法。该算法是一个迭代算法,会重复执行以下操作:
-
将字体子集的补丁映射解析为可用补丁列表。
-
检查是否有任何可用补丁与要渲染的内容匹配。
-
选择一个可用补丁,加载并应用它。
此过程会重复,直到不再有相关补丁。由于应用补丁可能改变 嵌入字体文件中的补丁 映射,因此在每次迭代中,都会重新解析字体子集当前版本中的补丁映射,以查看还有哪些补丁 可用。因此,在每次迭代中,字体子集都是可用补丁的事实来源,并且 完整封装了扩充过程的当前 状态。
4.1. 补丁失效
嵌入字体子集中的补丁映射为每个补丁编码一种失效模式。补丁的失效 模式 标记应用该补丁后,哪些其他补丁将不再有效。扩展算法使用这种 失效模式来确定哪些补丁兼容,并影响选择顺序。补丁 应用期间的有效性由 § 5.3 补丁映射 表中的兼容性 ID 强制保证。每个补丁内部都编码一个兼容性 ID, 该 ID 必须与列出该补丁的§ 5.3 补丁映射表中的兼容性 ID 相匹配。
共有三种失效模式:
-
完全 失效:应用该补丁时,当前列在字体子集中的所有其他补丁都会 失效。 'IFT ' 和 'IFTX' § 5.3 补丁映射 表中的兼容性 ID 都会更改。
-
部分 失效:应用该补丁时,同一§ 5.3 补丁映射表中的所有其他补丁都会失效。 只有包含 该补丁的§ 5.3 补丁映射表的兼容性 ID 会更改。
-
不 失效:应用该补丁不会使任何其他补丁失效。 'IFT ' 和 'IFTX' § 5.3 补丁映射表的兼容性 ID 不会更改。
特定补丁的失效模式编码在其格式编号中,该编号可在 § 6.1 格式摘要中找到。
4.2. 默认布局特性
大多数文本塑形器都有一组始终启用、因此在 增量加载字体中始终必需的布局特性。附录 A:默认特性标签汇总了一组 截至本文撰写时,已知在常见塑形器实现中默认必需的特性。 当形成一个字体子集定义作为扩展算法的输入时,客户端 通常应在子集定义中包含附录 A:默认特性 标签中的所有特性。但是,在某些情况下,客户端可能 知道将使用的特定塑形器不会使用附录 A:默认特性标签中的某些特性,并可 选择将这些未使用的特性排除在子集定义之外。
4.3. 增量 字体扩展算法
客户端使用以下算法扩展增量字体子集,使其覆盖更多 码位、布局特性和/或设计空间。该算法每次增量选择并应用一个补丁, 并按需加载 补丁。作为减少网络往返次数的重要优化,它允许为最终 需要的所有补丁启动加载。这些补丁有两种形式:
-
第一,补丁映射条目 可以列出多个 URL 字符串,其中第一个之后的每个 URL 字符串也应加载, 因为后续迭代会需要它们。
-
第二,使用 § 4.1 补丁失效确定可以为哪些 补丁启动加载。任何与目标子集 定义匹配,并且根据§ 4.1 补丁失效不会被下一个要应用的补丁 使之失效的补丁,都可预期在后续迭代中 需要。
扩展增量字体子集
该算法的输入为:
-
initial font URL:标识font subset所派生自的初始增量 字体位置的URL。
-
target subset definition:客户端希望将 font subset扩展至覆盖范围的字体子集定义。
该算法输出:
-
extended font subset:font subset的扩展版本。它可能是,也可能不是 增量 字体。
算法如下:
-
将 extended font subset 设置为 font subset。
-
从 extended font subset 中加载 'IFT ' 和 'IFTX'(如果存在)映射表。这两个 表都采用§ 5.3 补丁映射表格式。检查它们是否符合 § 5.3 补丁映射表中的要求。如果任一表无效,则调用处理 错误。如果 extended font subset 没有 'IFT ' 表,则它 不是增量 字体且无法扩展,返回 extended font subset。
-
如果 'IFT ' 中的兼容性 ID 等于 'IFTX' 中的兼容性 ID,则这是一个错误,调用 处理错误。
-
对于表 'IFT ' 和 'IFTX'(如果存在)中的每一个:通过调用解释格式 1 补丁映射或解释格式 2 补丁映射, 将表转换为条目列表。 将返回的条目列表连接成一个列表 entry list。
-
对于 entry list 中的每个 entry,以 entry 和 target subset definition 作为输入,调用检查条目交集;如果返回 false,则从 entry list 中移除 entry。此外,还要从 entry list 中移除补丁 URL 字符串已在先前算法执行期间加载并应用的 任何条目。
-
如果 entry list 为空,则扩展操作完成,返回 extended font subset。
-
根据每个条目的补丁格式的失效模式,将 entry list 中的条目分为 3 个列表: full invalidation entry list、partial invalidation entry list 和 no invalidation entry list。
-
按照以下过程选择一个 entry:
-
如果 full invalidation entry list 不为空,则恰好选择其中一个 条目。按照 § 4.4 选择失效补丁中的条件选择该单个条目。
-
否则,如果 partial invalidation entry list 不为空,则恰好选择其中一个 条目。 按照 § 4.4 选择失效 补丁中的条件选择该单个条目。
-
否则,选择 no invalidation entry list 中在 补丁映射内最先列出的条目。对于 § 5.3.1 补丁映射表:格式 1,这是条目索引 最小的条目。对于 § 5.3.2 补丁映射表:格式 2,这是在entries数组中最先出现的条目。 如果 IFT 和 IFTX 表中的条目都是候选项,则认为 IFT 中的所有条目都排在 IFTX 中的条目之前。
-
-
通过以 initial font URL 作为初始字体 URL、以 entry 中第一个补丁 URL 字符串作为 补丁 URL 字符串,调用加载补丁文件,开始加载 patch file(如果此前尚未启动)。如果 entry 中还有其他 URL 字符串, 则使用相同过程启动对它们的加载。这些 URL 可能在本算法后续迭代中使用。 此外,客户端还可以选择使用相同过程,为 entry list 中任何不会被 entry使之失效的条目启动加载。 客户端在本算法单次执行期间可以加载并应用的补丁总数限制为:
这些补丁可在本算法单次调用期间加载并应用。如果任一计数 超过限制,则这是一个错误,调用处理 错误。
-
patch file 加载完成后,使用 § 6 字体补丁格式中适当的应用 算法(与 entry 中的补丁格式相匹配),使用补丁 URL 字符串和 entry 中的兼容性 id,将 patch file 应用于 extended font subset。如果加载 patch file 时出错,则按照处理错误进行处理。
-
转到步骤 2。
注:在步骤 9 中,客户端可以选择并行启动加载 不会被当前所选条目 使之失效的补丁。这类补丁几乎总会在后续迭代中需要(因为它们不会 失效),并且并行加载 可通过消除往返显著提高性能。以这种 方式启动加载时,请求发起顺序需遵循步骤 8 中指定的顺序。
注:如果剩余相交条目全部是 不失效条目,则无需在后续迭代中重复步骤 5 的交集检查(这是因为不失效补丁在应用时不会改变 可用补丁列表, 只会移除其自身条目)。此外,作为一种优化,客户端可能 希望将剩余相交的不失效条目作为单个批处理操作来应用补丁。 由于以字形为键的补丁的性质,可以直接在一次处理中构造 依次应用多个补丁的最终结果。这样可避免针对每个补丁反复重新合成新的 glyf/loca/CFF/CFF2 表,并可显著减少所需计算总量。
检查条目交集
该算法的输入为:
该算法输出:
-
intersects:如果 subset definition 与 mapping entry 相交,则为 true, 否则为 false。
算法如下:
-
对于 subset definition 中的每个集合(码位、特性标签、 设计空间),检查该集合是否与 mapping entry 的子集定义中的对应集合相交。集合 在以下情况下相交:
子集定义集合为空 子集定义集合不为空 映射条目集合为空 true true 映射条目集合不为空 false 如果两个集合相交,则为 true 检查设计空间集合是否相交时,只要至少有一对 相交片段 (标签相等且范围相交),它们就相交。
-
如果步骤 1 中检查的一个或多个集合不相交,则为 intersects 返回 false。
-
如果 mapping entry 中没有子条目,则为 intersects 返回 true。
-
对于 mapping entry 中引用的每个 子条目,以 subset definition 为输入调用检查条目交集。如果 子条目匹配模式为合取模式,则仅当所有调用都返回 true 时,才为 intersects 返回 true。 否则,仅当至少一个调用返回 true 时才返回 true。
下表表示一个补丁映射,并展示对 各种条目进行交集检查的预期结果。在这些示例中, 未指定集合(即码位、特性标签、设计空间、子条目)时,假定它为空集合。
| 索引 | 映射条目 | 子集定义 | 是否相交? |
|---|---|---|---|
| 0 |
subset definition {
code points: {1, 2, 3},
},
match mode: disjunctive,
|
code points: {2},
| true |
| 1 |
subset definition {
code points: {4, 5, 6},
},
match mode: disjunctive,
|
code points: {2},
| false |
| 2 |
subset definition {
code points: {1, 2, 3},
},
match mode: disjunctive,
|
code points: {2},
feature tags: {smcp},
| true |
| 3 |
subset definition: {
code points: {1, 2, 3},
},
match mode: disjunctive,
|
feature tags: {smcp},
| false |
| 4 |
subset definition: {
code points: {1, 2, 3},
},
child entry indices: {1},
match mode: disjunctive,
|
code points: {5},
| false |
| 5 |
subset definition: {
},
child entry indices: {0, 1},
match mode: conjunctive,
|
code points: {2},
| false |
| 6 |
subset definition: {
},
child entry indices: {0, 1},
match mode: conjunctive,
|
code points: {2, 6},
| true |
加载 补丁文件
该算法的输入为:
-
patch URL string:一个URL 字符串(使用 [UTF-8] 编码),用于标识要加载的补丁 文件。它可以是相对 URL。
-
initial font URL:标识font subset所派生自的初始增量 字体位置的URL。
该算法输出:
-
patch file:由 patch URL string 标识的内容(字节)。
算法如下:
-
使用 URL 标准 § url-parsing,将 patch URL string 解析为URL 记录。initial font URL 是基础 URL,编码为 UTF-8。将结果 存储在 target URL 中。如果 URL 解析失败,则返回错误。
-
使用实现用户代理的获取能力检索 target URL 的内容。 对于 Web 浏览器,应使用 [FETCH]。使用 [FETCH] 时,请求配置如下:
-
将method设置为 GET。
-
将url设置为 target URL。
-
将client设置为用于获取 initial font URL 的请求对象中的客户端。
-
将initiator type设置为 "font"。
-
将destination设置为 "font"。
-
将referrer设置为 initial font URL。
-
将mode设置为 "cors"。
获取该请求, 其中processResponseConsumeBody为本 算法的步骤 3。
对于不使用[FETCH]的 实现,请使用可用的获取能力向 target URL 发出请求。 在存在等效概念时,复制上文指定的基于 [FETCH] 的 设置。使用该获取操作的结果继续执行步骤 3。
-
-
processResponseConsumeBody 接收响应 以及 null、failure 或字节序列。如果收到字节序列, 则将这些字节作为 patch file 返回。如果收到 null,则将空字节数组 作为 patch file 返回。 否则返回错误。
注:这些获取设置旨在与 CSS 字体加载所使用的设置相匹配,参见 CSS 字体 4 § 4.8.2 字体获取要求,但存在两处差异: 第一,referrer 设置为初始字体的 URL;第二,URL 解析使用初始字体作为基础, 而不是样式表。
处理 错误
如果扩展字体子集的过程因错误而失败,则字体内的一些数据可能 未完全加载,因此, 渲染依赖缺失数据的内容可能产生错误结果。客户端 可以选择继续使用 该字体,但只能将其用于渲染根据 § 4.6 确定字体能够渲染的内容已完全 加载的码位、特性和设计空间。 所有其他内容的渲染应按照客户端正常的回退逻辑,回退到其他字体。
如果错误发生在加载补丁文件期间,则客户端可以继续尝试 扩展字体子集。在扩展增量字体子集的步骤 8 中, 在每个失效分组内,任何加载失败的补丁都会被排除在选择范围之外。 如果某个失效组非空并正在从中选择,但其中只包含失败的补丁,则 扩展已经失败且无法 继续。
对于所有其他错误,客户端 不得尝试进一步扩展字体子集。
4.4. 选择失效补丁
在执行扩展增量字体子集 算法期间,某些情况下需要从候选条目列表中选择一个失效(完全或部分) 补丁条目。所使用的选择条件会直接影响 执行扩展所需的往返总次数。往返代价高昂,因此,为获得最佳性能,应以 尽量减少所需往返总次数的方式选择补丁。
以下选择条件 可尽量减少往返次数,并且客户端在 扩展增量字体 子集的步骤 8 中选择单个部分失效或完全失效补丁时必须使用:
-
如果一个或多个候选条目的关联补丁文件此前已由 扩展增量字体 子集的步骤 9 加载, 则在以下步骤中,仅考虑补丁文件已加载或当前正在加载的候选条目。否则考虑所有 候选条目。
-
对于每个候选条目:将该条目的子集 定义与通过所引用子条目图可达的所有子条目的子集定义求并集,计算出总子集定义。
-
对于每个候选条目:计算总子集定义与 目标子集定义之间的集合交集。
-
找到一个条目,使其在步骤 2 中得到的交集不是任何其他交集的真子集。
-
查找与步骤 3 中找到的条目位于同一补丁映射中,并且具有相同交集的任何其他条目。从这组 条目(包括步骤 3 选中的条目)中,最终选择在补丁映射中最先列出的条目。对于 § 5.3.1 补丁映射表:格式 1,这是 条目索引最小的条目。对于 § 5.3.2 补丁映射表:格式 2,这是在entries数组中最先出现的条目。
注:寻找满足 步骤 3 条件的条目的一个快速高效方法,是先按码位 集合交集的大小、再按特性标签集合交集的大小,最后按设计空间 交集的大小对条目进行降序排序。该排序中的第一个条目 保证不是任何其他条目的真子集,因为任何真超集至少都必须多一个 项目。这种方法还有一个 额外好处,即会选择能够添加客户端当前所请求最多数据的补丁。
4.5. 目标子集定义
扩展增量字体子集 算法将基于客户端希望渲染的某些内容形成的目标子集定义作为输入。 客户端可以选择为整体内容形成一个子集定义,并 运行一次扩展算法。或者,客户端也可以将内容拆分为较小的文本段, 为每个文本段形成一个子集 定义,并针对每个较小的子集定义运行 扩展算法。只要满足以下条件,两种方法最终都会生成 能等效渲染整体内容的字体:
4.6. 确定字体能够渲染的内容
给定某个增量字体(无论是初始字体还是已部分扩展的字体),客户端可能 希望知道该字体在当前状态下 能够渲染哪些内容。在客户端试图确定文本中的哪些部分 应使用回退字体时,这一点尤为重要。
在回退处理期间,客户端通常会检查字体的cmap表,以确定 支持哪些码位;但是,在 IFT 字体中,由于§ 6.3 以字形为键补丁的工作方式, cmap表可能包含 尚未加载相应字形数据的码位映射。因此,客户端不应 仅依赖cmap表 来确定码位是否存在。相反,客户端可使用以下过程,检查 增量字体能够渲染某些内容中的哪些部分:
-
将内容拆分成塑形单元(参见 § 4.5 目标子集定义),文本 塑形期间将按这些单元处理内容。
-
对每个塑形单元执行两项检查:
-
任何同时通过两项检查的塑形单元 都可使用该字体完整渲染。
客户端还可能希望以比塑形单元更细的粒度了解字体能够渲染哪些内容。 以下伪代码 展示了一种可能的方法,可将未通过上述检查的塑形单元拆分为 可使用 增量字体渲染的文本段:
# 返回 shaping_unit 中受 ift_font 支持且可安全使用其渲染的文本段列表, # 每个文本段为包含端点的 [start, end]。 # # shaping_unit 是一个数组,其中每个项目都包含一个码位、关联的布局特性列表, # 以及渲染该码位时所使用的设计空间点。 def supported_spans ( shaping_unit , ift_font ): current_start = current_end = current_subset_def = None supported_spans = [] i = 0 while i < shaping_unit . length (): if current_subset_def is None : current_subset_def = SubsetDefinition () current_start = i current_end = i current_subset_def . add ( shaping_unit . codepoint_at ( i ), shaping_unit . features_at ( i ), shaping_unit . design_space_point_at ( i )) if supports_subset_def ( ift_font , current_subset_def ): i += 1 continue if current_end > current_start : supported_spans . append ( Span ( current_start , current_end - 1 )) # 不递增 i,以便在下一次迭代中单独检查当前码位。 # else : i += 1 current_start = current_end = current_subset_def = None return supported_spans # 如果 ift_font 支持渲染 subset_def 所覆盖的内容,则返回 true。 def supports_subset_def ( ift_font , subset_def ): # 仅当以下两项检查均为 true 时才返回 true: # - subset_def 中的每个码位都由 ift_font 的 cmap 表映射到非 '0' 的字形 id。 # - 使用 subset_def 对 ift_font 执行“扩展增量字体子集”算法,并在步骤 6 停止后, # 条目列表为空。
来自塑形单元、 且未被返回文本段之一覆盖的任何文本,都不受增量字体支持,应 使用回退字体渲染。每个文本段都应单独进行塑形(即每个文本段成为新的 塑形单元)。 由于此方法会拆分塑形单元,原始字体的某些特性(例如多码位 替换)可能无法 保留。如果客户端正确遵循扩展增量字体子集 算法,并使用按照§ 4.5 目标子集定义形成的子集定义, 则缺失的数据将被加载,这种情况只会在相关补丁正在 加载期间暂时发生。缺失补丁到达并应用后,受影响码位的渲染结果 可能因替换而发生变化。
注:上面的 "supported_spans(...)" 检查并非 用于驱动增量字体扩展。有关为执行扩展增量字体子集形成目标子集定义的指导, 请参阅 § 4.5 目标子集定义。
4.7. 完全扩展 字体
本节定义一种算法,可用于将增量字体转换为完全扩展的 非增量字体。此 过程会加载增量字体提供的所有可用数据,并生成一个不再有任何 补丁可应用的静态字体文件。
完全扩展字体子集
该算法的输入为:
该算法输出:
-
expanded font:一个非增量的 [open-type] 字体。
算法如下:
4.8. 缓存已扩展的增量字体
已扩展的增量字体包含按照本节过程执行未来任何扩展 操作所需的全部状态。因此,如果客户端需要存储或缓存增量字体以供将来使用, 只需 存储最近一次应用扩展算法所生成的字体二进制文件。 无需保留初始 字体或之前扩展生成的任何版本。
5. 字体格式扩展
增量字体 遵循现有的 OpenType 格式,但包含两个由 4 字节标签 'IFT ' 和 'IFTX' 标识的新表。 这两个新表都是补丁映射。所有增量字体至少包含一个 'IFT ' 表。'IFTX' 表是可选的。当两个表都 存在时,字体整体的映射是两个表映射的并集。这两个新 表仅在本 规范中使用,不会添加到 OpenType 规范中。
注:允许将映射拆分到两个 不同的表中,使增量字体更容易使用多种 补丁类型。例如,一种类型的所有补丁可在 'IFT ' 表中指定,第二种类型的所有补丁可在 'IFTX' 表中指定。这些补丁可以只更新其中一个映射表,从而避免产生冲突 更新。
5.1. 增量字体传输与字体压缩格式
在 Web 上使用字体时,通常会使用 [WOFF] 或 [WOFF2] 等压缩格式对其进行压缩。 只要满足以下条件,这类格式就可用于压缩增量字体传输编码中使用的初始字体文件:
-
通过压缩格式对字体进行编码后再解码的过程,不会修改每个表 的字节。
-
由于增量字体传输扩展算法(§ 4 扩展字体子集)专门作用于未压缩字体 文件,因此在尝试扩展压缩字体之前,需要先将其解码。
对于 [WOFF2],需要特别 注意。如果增量字体将使用 WOFF2 编码以进行传输:
-
如果 WOFF2 编码将包含经过转换的 glyf 和 loca 表(WOFF 2.0 § 5.1 转换后的 glyf 表格式),那么增量 字体不应包含会修改 glyf 或 loca 表的§ 6.2 以表为键补丁。WOFF2 格式不 保证解码经过转换的 glyf 和 loca 表后得到的具体字节。§ 6.3 以字形为键补丁可 与经过转换的 glyf 和 loca 表结合使用。
-
WOFF2 编码器可以按照 WOFF 2.0 § 5 压缩数据格式中定义的 标准过程处理 'IFT ' 和 'IFTX' 表,并使用 brotli 编码。
注:鉴于即使禁用 glyf/loca 转换,WOFF2 的压缩效果仍优于 WOFF, 通常建议 使用 WOFF2 压缩 IFT 字体;当编码使用 以表为键的补丁更新这些表时,应禁用 glyf/loca 转换。
5.2. CFF 和 CFF2 增量字体
对于字形轮廓存储在 CFF 或 CFF2 表中的增量字体,还存在一些 额外限制:
-
增量 字体中的 CFF 和 CFF2 表必须将 CharStrings INDEX 数据结构作为表的最后一个元素 (其后只能有用于填充至四字节字边界的 0)。
-
增量字体中的 CFF 和 CFF2 表 不得包含任何与 CharStrings INDEX 数据 结构重叠的其他数据元素。
-
'IFT ' 补丁映射表必须 包含 CFF 和/或 CFF2 表中 CharStrings INDEX 数据起始位置的偏移 (cffCharStringsOffset、cff2CharStringsOffset、cffCharStringsOffset 或 cff2CharStringsOffset)。
这三项要求可显著简化 CFF 和 CFF2 表上以字形为键的补丁应用实现。 它们允许将字形数据插入 CFF/CFF2 表,而无需更改 CFF/CFF2 表中 编码的任何其他数据元素的偏移。 存储的 CFF/CFF2 偏移使客户端无需解析 CFF/CFF2 表的任何其他部分, 即可找到 CharStrings INDEX。
注:在某些情况下,CFF 或 CFF2 表可能会由 以表为键的补丁添加或更改。在这些情况下,补丁 还需要添加或更新 charstrings 偏移,以反映新增或更改后的内容。
CFF 或 CFF2 子例程化与增量字体传输兼容。但是,简单的 子例程化往往会 减小补丁大小,却增加初始文件大小。此外,WOFF2 通常被证明可以比其子例程化版本 更有效地压缩 未子例程化的字体(代价是未压缩文件显著 更大)。因此,建议避免使用子例程化、适度使用子例程化,或根据 IFT 的特定需求 对其进行定制。
5.3. 补丁映射表
补丁映射采用以下两种 格式之一进行编码:
-
格式 1:功能有限,但编码更紧凑。它对从字形 id 到 补丁 URL 字符串的一一映射进行编码。它不 支持带设计空间的字体子集定义,也不支持 子集定义彼此重叠的条目。
-
格式 2:可编码任意映射,包括带设计空间或子集 定义彼此重叠的映射。但是,它 通常不如格式 1 紧凑。
每种格式都定义一种算法,用于解释使用该格式编码的字节,以生成它所表示的 条目列表。扩展增量字体子集 算法调用这些解释算法,并对得到的条目列表进行操作。 编码字节始终是补丁映射的事实来源。子集扩展期间应用补丁 会改变补丁映射的编码 字节,因此从编码字节派生出的条目列表也会改变。扩展 算法在每次迭代开始时都会重新解释 编码字节,以获取上一次迭代中所做的任何更改。
5.3.1. 补丁映射表:格式 1
格式 1 补丁映射 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8 | format | 设为 1,标识其为格式 1。 |
| uint24 | reserved | 未使用,设为 0。 |
| uint8 | flags | 用于指示可选字段是否存在的标志。 如果设置了位 0(最低有效位),则cffCharStringsOffset将 存在。 如果设置了位 1,则cff2CharStringsOffset将 存在。 |
| uint32 | compatibilityId[4] | 用于标识与此字体兼容的补丁的唯一 ID(参见§ 4.1 补丁失效)。编码器选择该 值。编码器应将其设置为一个此前在 编码 IFT 字体时从未使用过的随机值。 |
| uint16 | maxEntryIndex | 此表中编码的最大条目索引。 |
| uint16 | maxGlyphMapEntryIndex | 此表中编码的最大字形映射条目索引。 必须小于或等于 maxEntryIndex。 |
| uint24 | glyphCount |
提供映射的字形数量。
必须与字体文件中的字形数量相匹配。
注:字体中的字形数量 编码在字体文件中。截至本文撰写时,该值列在maxp表中; 但是,未来的字体格式扩展可能使用其他表来编码 字形 数量。 |
| Offset32 | glyphMapOffset | 指向字形映射子 表的偏移。偏移相对于此表的起始位置。 |
| Offset32 | featureMapOffset | 指向特性映射 子表的偏移。偏移相对于此表的起始位置。可以为 null(0)。 |
| uint8 | appliedEntriesBitMap[(maxEntryIndex + 8)/8] | 一个位图,用于跟踪哪些条目已应用。如果位 i 被设置,则
表明条目 i 的补丁已应用于此字体。位 0 是
appliedEntriesBitMap[0] 的最低有效位,而位 7 是
最高有效位。位 8 是 appliedEntriesBitMap[1] 的最低有效位,依此
类推。
|
| uint16 | urlTemplateLength | urlTemplate 字节数组的长度。 |
| uint8 | urlTemplate[urlTemplateLength] | 包含URL 模板的字节数组,该模板用于生成 与每个条目关联的 URL 字符串。 |
| uint8 | patchFormat | 指定由 urlTemplate 链接的补丁格式。必须设置为 § 6.1 格式 摘要表中的一个格式编号。 |
| uint32 | cffCharStringsOffset | 仅当flags的位 0(最低有效位)被设置时存在。 给出从 CFF 表起始位置到 CFF 表的 CharStrings INDEX 数据结构的偏移。 |
| uint32 | cff2CharStringsOffset | 仅当flags的位 1 被设置时存在。 给出从 CFF2 表起始位置到 CFF2 表的 CharStrings INDEX 数据结构的偏移。 |
注:glyphCount的 设计旨在与允许超过 65,535 个字形的拟议未来字体格式 扩展兼容。
字形映射编码:
字形映射表将字体中的每个字形索引与一个条目索引关联。
| 类型 | 名称 | 描述 |
|---|---|---|
| uint16 | firstMappedGlyph | 所有小于 firstMappedGlyph 的字形索引都隐式映射到条目索引 0。 |
| uint8/uint16 | entryIndex[glyphCount - firstMappedGlyph] | 字形 i 的条目索引存储在 entryIndex[i - firstMappedGlyph] 中。如果 maxEntryIndex 小于 256,则数组成员
为 uint8,否则为 uint16。
|
特性映射编码:
特性映射表将特性标签和 字形的组合与条目索引关联。
| 类型 | 名称 | 描述 |
|---|---|---|
| uint16 | featureCount | featureRecords 的数量。 |
| FeatureRecord | featureRecords[featureCount] | 为特定特性 标签提供映射。featureRecords按featureTag升序排序,且任何特性标签 最多出现一次。排序时,标签值解释为 4 字节大端无符号整数,并按整数值排序。 |
| EntryMapRecord | entryMapRecords[variable] | 为每个特性映射提供键(条目索引)。entryMapRecords 数组包含的条目数量, 等于 featureRecords数组中各entryMapCount字段之和,其中 entryMapRecords[0] 对应 featureRecords[0] 的第一个条目,entryMapRecords[featureRecord[0].entryMapCount] 对应 featureRecords[1] 的第一个条目,entryMapRecords[featureRecords[0].entryMapCount + featureRecord[1].entryMapCount]] 对应 featureRecords[2] 的第一个条目,依此类推。 |
FeatureRecord 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| Tag | featureTag | 此映射所对应的特性 标签。 |
| uint8/uint16 | firstNewEntryIndex | 如果maxEntryIndex小于 256,则为 uint8,否则 为 uint16。 此记录映射到的第一个条目索引。 |
| uint8/uint16 | entryMapCount | 如果maxEntryIndex小于 256,则为 uint8,否则 为 uint16。 与此特性关联的EntryMapRecord数量。 |
EntryMapRecord 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8/uint16 | firstEntryIndex | 如果maxEntryIndex小于 256,则为 uint8,否则 为 uint16。firstEntryIndex 和 lastEntryIndex 指定 一组字形映射 条目,这些条目构成此映射所创建条目的子集定义。 |
| uint8/uint16 | lastEntryIndex | 如果maxEntryIndex小于 256,则为 uint8,否则 为 uint16。 |
条目映射记录与任何大于或等于 firstEntryIndex 且小于 或等于 lastEntryIndex 的条目索引匹配。
5.3.1.1. 解释 格式 1
此算法用于将格式 1 补丁映射转换为补丁映射条目列表。
解释格式 1 补丁映射
该算法的输入为:
该算法输出:
-
entry list:补丁映射条目列表。
算法如下:
-
检查 patch map 数据是否完整且未被截断,format是否等于 1, 并且是否符合 § 5.3.1 补丁映射表:格式 1中的要求 (要求使用“必须”标记)。如果不符合,则返回错误。
-
对于entryIndex中的每个唯一 entry index:
-
如果 entry index 为 0,则这是一个特殊条目,用于标记已 存在于初始字体中的字形。跳过此 索引,不为其构建条目。
-
如果 entry index 大于maxGlyphMapEntryIndex,则此 条目无效,跳过此 entry index。
-
如果appliedEntriesBitMap中 entry index 对应的位被设为 1,则跳过此 entry index。
-
收集映射到 entry index 的字形索引集合。
-
使用 font subset 的cmap表中的码位到 字形映射,将字形索引集合转换为 Unicode 码位集合。忽略任何未被 cmap映射的字形索引。 多个码位可以映射到同一字形 id。应包含与某个字形 关联的所有码位。
-
通过以urlTemplate和 entry index 作为输入调用扩展 URL 模板,将 entry index 转换为 URL 字符串。 如果模板扩展产生错误,则返回 错误。
-
如果 Unicode 码位集合为空,则跳过此 entry index。
-
向 entry list 添加一个条目,其子集 定义仅包含 Unicode 码位 集合,并映射到生成的 URL 字符串、patchFormat指定的补丁格式,以及compatibilityId。
-
-
如果featureMapOffset不为 null,则对于 featureRecords 和entryMapRecords中的每个FeatureRecord及其 关联的EntryMapRecord:
-
如果任何FeatureRecord的featureTag 小于或等于列表中先前出现的任何FeatureRecord的featureTag,则该 FeatureRecord 无效。跳过所有关联的EntryMapRecord。 排序时,标签值解释为 4 字节大端无符号整数, 并按整数值排序。
-
计算 mapped entry index。与某个 FeatureRecord 关联的第一个EntryMapRecord的映射条目索引为 FeatureRecord::firstNewEntryIndex,第二个为 FeatureRecord::firstNewEntryIndex + 1,依此类推。最后一个为FeatureRecord::firstNewEntryIndex + entryMapCount - 1。
-
如果计算得到的 mapped entry index 小于或等于maxGlyphMapEntryIndex,或 大于maxEntryIndex,则此EntryMapRecord无效,跳过它。
-
如果EntryMapRecord::firstEntryIndex大于 EntryMapRecord::lastEntryIndex,则此EntryMapRecord无效,跳过它。
-
通过以urlTemplate和 mapped entry index 作为输入调用扩展 URL 模板,将 mapped entry index 转换为 URL 字符串。 如果模板扩展产生错误,则返回 错误。
-
如果appliedEntriesBitMap中 mapped entry index 对应的位被设为 1,则跳过此条目。
-
构造 Unicode 码位集合。对于位于EntryMapRecord::firstEntryIndex (包含)和EntryMapRecord::lastEntryIndex (包含)之间的每个 entry index:
-
如果 entry index 大于maxGlyphMapEntryIndex, 则此EntryMapRecord无效, 跳过它。
-
将在步骤 2 中计算的、与 entry index 关联的 Unicode 码位集合 添加到该集合。如果 entry index 因为它为 0 或因appliedEntriesBitMap 而被跳过,则 按照未跳过的情况计算其关联码位集合。
-
-
如果构造的 Unicode 码位集合为空,则此EntryMapRecord无效,跳过 它。
-
向 entry list 添加一个条目,该条目映射到生成的 URL 字符串、patchFormat指定的补丁格式,以及compatibilityId;或者,如果 entry list 中已有一个条目具有与生成的 URL 字符串相同的 补丁 URL 字符串,则改为修改现有条目。将构造的 Unicode 码位集合和featureTag添加到新条目或 现有条目的子集定义中。
-
-
返回 entry list。
注:虽然编码不要求包含 [0, maxEntryIndex] 中所有条目索引的条目,但 为获得最大紧凑性,建议这样做。
5.3.1.2. 从格式 1 中移除条目
此算法用于从格式 1 补丁映射中移除条目。此移除操作会修改 补丁映射的字节,但不会 改变字节数量。
从格式 1 补丁映射中移除条目
该算法的输入为:
-
patch map:一个使用格式 1 补丁映射编码的补丁映射。此过程可修改 该补丁映射。
-
patch URL string:标识要移除条目的补丁 URL 字符串。
算法如下:
-
检查 patch map 的format是否等于 1,并且是否符合 § 5.3.1 补丁映射表:格式 1中的要求。如果不符合, 则返回错误。
-
对于 patch map 的entryIndex中的每个唯一 entry index:
-
如果appliedEntriesBitMap中 entry index 对应的位被设为 1,则跳过此 entry index。
-
通过以urlTemplate和 entry index 作为输入调用扩展 URL 模板,将 entry index 转换为 URL 字符串。 如果模板扩展产生错误,则返回 错误。
-
如果生成的 URL 字符串等于 patch URL string,则将 appliedEntriesBitMap 中 entry index 对应的位设为 1。
-
5.3.2. 补丁映射表:格式 2
格式 2 补丁映射 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8 | format | 设为 2,标识其为格式 2。 |
| uint24 | reserved | 未使用,设为 0。 |
| uint8 | flags | 用于指示可选字段是否存在的标志。 如果设置了位 0(最低有效位),则cffCharStringsOffset将 存在。 如果设置了位 1,则cff2CharStringsOffset将 存在。 |
| uint32 | compatibilityId[4] | 用于标识与此字体兼容的补丁的唯一 ID(参见§ 4.1 补丁失效)。编码器选择该 值。编码器应将其设置为一个此前在 编码 IFT 字体时从未使用过的随机值。 |
| uint8 | defaultPatchFormat | 指定由 urlTemplate 链接的补丁格式(除非被条目覆盖)。 必须设置为 § 6.1 格式 摘要表中的一个格式编号。 |
| uint24 | entryCount | 此表中编码的条目数量。 |
| Offset32 | entries | 指向映射 条目子表的偏移。偏移相对于此表的起始位置。 |
| Offset32 | entryIdStringData | 指向包含所有条目 ID 字符串串接结果的数据块的偏移。 可以为 null(0)。偏移相对于此表的起始位置。 |
| uint16 | urlTemplateLength | urlTemplate 字节数组的长度。 |
| uint8 | urlTemplate[urlTemplateLength] | 包含URL 模板的字节数组,该模板用于生成 与每个条目关联的 URL 字符串。 |
| uint32 | cffCharStringsOffset | 仅当flags的位 0(最低有效位)被设置时存在。 给出从 CFF 表起始位置到 CFF 表的 CharStrings INDEX 数据结构的偏移。 |
| uint32 | cff2CharStringsOffset | 仅当flags的位 1 被设置时存在。 给出从 CFF2 表起始位置到 CFF2 表的 CharStrings INDEX 数据结构的偏移。 |
映射条目 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8 | entries[variable] | 包含entryCount个映射条目编码字节的字节数组。每个条目具有可变 长度,其长度按照解释格式 2 补丁映射 条目确定。 |
映射条目编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8 | formatFlags | 位字段。位 0(最低有效位)到位 5 表示是否存在可选 字段。如果设置了位 6,则忽略此条目。 位 7 保留供未来使用,并设为 0。 |
| uint8 | featureCount | featureTags 列表中的特性标签数量。仅当formatFlags的位 0 被设置时存在。 |
| Tag | featureTags[featureCount] | 条目字体子集定义中的特性 标签列表。仅当formatFlags的位 0 被设置时存在。 |
| uint16 | designSpaceCount | 设计空间列表中的元素数量。仅当formatFlags 的位 0 被设置时存在。 |
| 设计 空间片段 | designSpaceSegments[designSpaceCount] | 条目字体子集 定义中的设计空间片段列表。仅当formatFlags的位 0 被设置时存在。 |
| uint8 | childEntryMatchModeAndCount | 此字段和下一个字段用于向此 条目的交集检查添加条件,这些条件取决于之前的条目是否相交。 最高有效位用于表示匹配模式。如果设置该位,则条件 为合取模式,否则为析取模式。 剩余 7 位解释为无符号整数,表示 childEntryIndices 列表中的条目索引数量。仅当formatFlags的位 1 被设置时存在。 |
| uint24 | childEntryIndices[0b01111111 & childEntryMatchModeAndCount] | 每个值都是entries数组中某个条目的索引,将检查该条目的交集 以确定此条目的交集。 只能引用在entries中位于此映射条目之前的条目。 仅当formatFlags的位 1 被设置时存在。 |
| int24 | entryIdDelta[variable] | 用于计算此条目的 URL 字符串 ID 的有符号增量列表。如果存在, 则至少包含一个增量。如果某个增量的最低 有效位被设置,则后面还有一个增量。此过程会重复,直到遇到 最低有效位被清除的增量。每个增量对应的条目 id 等于上次计算的条目 id + 1 + floor(entryIdDelta / 2)。仅当formatFlags 的位 2 被设置且entryIdStringData为 null(0)时存在。 如果不存在且entryIdStringData为 null(0),则 此条目有一个 id,并假定 增量为 0。 |
| uint24 | entryIdStringLength[variable] | 此条目的每个 id 字符串在entryIdStringData数据块中占用的字节数。 如果某个长度值的最高有效位被设置,则后面还有一个长度值。此过程会重复, 直到遇到最高有效位被清除的长度值。实际长度值存储在 每个值的最低 23 位中。仅当formatFlags 的位 2 被设置且entryIdStringData不为 null(0)时存在。如果 存在,则至少有一个长度值。如果不存在 且entryIdStringData不为 null(0), 则有一个 id 字符串,其值设为上一个条目的最后一个 id 字符串;对于第一个条目,则设为空字符串。 |
| uint8 | patchFormat | 指定此条目链接到的补丁格式。使用§ 6.1 格式摘要表中的 ID 编号。 覆盖defaultPatchFormat。 仅当formatFlags的位 3 被设置时存在。 |
| uint16/uint24 | bias | 添加到码位集合中所有码位值的偏置值。 如果格式位 4 为 0 且位 5 为 1,则此字段存在且为 uint16。 如果格式位 4 为 1 且位 5 为 1,则此字段存在且为 uint24。 否则不存在。 |
| uint8 | codePoints[variable] | 此映射的码位集合。编码为稀疏位集合。 仅当formatFlags的位 4 和/或位 5 被设置时存在。长度 按照§ 5.3.2.3 稀疏位集合中的解码过程确定。 |
如果编码器生成的补丁将存储在文件系统中并随后提供服务,则建议 仅使用数字条目 ID (通过entryIdDelta),因为它们通常会产生最小的 格式 2 补丁映射编码。字符串 ID 适用于补丁未预先存储,并且可以使用 ID 字符串编码有关所请求补丁 的信息的场景。
设计空间 片段编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| Tag | tag | 轴标签值。 |
| Fixed | start | 片段的起点(包含)。该值使用用户轴标度:OpenType 规范 § otvaroverview#coordinate-scales-and-normalization。 |
| Fixed | end | 片段的终点(包含)。必须大于或等于 start。该值使用用户轴标度:OpenType 规范 § otvaroverview#coordinate-scales-and-normalization。 |
5.3.2.1. 解释 格式 2
解释格式 2 补丁映射
该算法的输入为:
该算法输出:
-
entry list:补丁映射条目列表。
算法如下:
-
检查 patch map 的format是否等于 2,并且是否符合 § 5.3.2 补丁映射表:格式 2中的要求(要求 使用“必须”标记)。如果不符合,则返回错误。
-
如果entryIdStringData偏移为 0,则 将 last entry id 初始化为 0。否则将 其初始化为空字节字符串。将 current byte 设为 0,将 current id string byte 设为 0。
-
将 entry list 和 prior entry list 初始化为空列表。
-
调用解释格式 2 补丁映射 条目 entryCount 次。对于每次调用:
-
传入 prior entry list、patch map 中从entries[current byte] 开始到 patch map 末尾的字节; 如果entryIdStringData非零,则传入 patch map 中从entryIdStringData[current id string byte] 开始 到 patch map 末尾的字节; 还传入 last entry id、defaultPatchFormat 和urlTemplate。
-
将 last entry id 设置为返回的条目 id。
-
将返回的已消耗字节数加到 current byte。
-
将返回的已消耗 id 字符串字节数加到 current id string byte。
-
将返回的条目添加到 prior entry list。
-
如果返回的 ignored 值为 false,则将返回条目的兼容性 ID 设置为compatibilityId,并将该条目添加到 entry list。
-
-
返回 entry list。
解释格式 2 补丁映射条目
该算法的输入为:
-
prior entry list:位于此条目之前的条目列表。
-
entry bytes:包含已编码映射条目的字节数组。
-
id string bytes(可选):包含条目 ID 字符串的字节数组。
-
last entry id:上次生成的条目 id。
-
default patch format:未指定补丁格式时的默认补丁格式。
-
url template:用于定位补丁的 URL 模板。
该算法输出:
-
entry id:此条目最后生成的数字或字符串 id。
-
entry:单个条目。
-
consumed bytes:用于编码该条目的字节数。
-
consumed id string bytes:用于编码条目 id 字符串的字节数。
-
ignored:如果为 true,则应忽略此条目。
算法如下:
-
对于所有步骤,每当从 entry bytes 加载数据时,就将读取的 字节数增加到 consumed bytes。
-
设置 entry id = last entry id,consumed id string bytes = 0。
-
将 entry 的补丁格式设置为 default patch format。
-
向 entry 添加一个字体子集定义,并将所有 集合初始化为空。
-
从 entry bytes 读取formatFlags。
-
如果formatFlags的位 0 被设置,则存在特性标签和 设计空间列表:
-
从 entry bytes 读取由featureCount和featureTags 指定的特性标签列表,并将加载的标签添加到 entry 中第一个字体子集 定义中。
-
从 entry bytes 读取由designSpaceCount和designSpaceSegments指定的设计空间片段列表, 并将这些设计空间片段添加到 entry 中第一个字体子集 定义中。每个 片段定义一个由tag标识的轴上,从start到end(包含端点)的区间。 如果任何片段的start大于end,则此编码无效,返回 错误。
-
-
如果formatFlags的位 1 被设置,则存在复制索引 列表:
-
如果childEntryMatchModeAndCount 的最高有效位被设置,则将 entry 的匹配模式设为合取模式, 否则设为析取模式。
-
从 entry bytes 读取由childEntryMatchModeAndCount 和childEntryIndices指定的子条目索引列表。
-
子条目索引引用此前加载的条目。0 是 prior entry list 中的第一个映射条目, 1 是第二个, 依此类推。对于childEntryIndices中的每个值, 在 prior entry list 中找到索引匹配的条目。 向 entry 添加对该条目的引用。如果某个childEntryIndices大于或 等于 prior entry list 的长度,则此编码无效,返回错误。
-
-
如果formatFlags的位 2 被设置,则存在 id 增量或 id 字符串长度:
-
如果不存在 id string bytes,则:
-
读取下一个entryIdDelta值。
-
将 entry id 设置为
entry id + 1 + floor(delta value / 2)。 如果 entry id 为负数或大于 4,294,967,295,则此编码 无效,返回错误。 -
通过以 url template 和 entry id 作为输入调用扩展 URL 模板,将 entry id 转换为 URL 字符串。如果模板扩展 产生错误,则返回错误。
-
将生成的补丁 URL 字符串添加到 entry。
-
如果读取的增量值的最低有效位被设置,则重复步骤 8。
-
-
否则,如果存在 id string bytes,则:
-
读取下一个entryIdStringLength值。
-
将最低 23 位解释为无符号整数,并从 id string bytes 中读取相应数量的字节。 将 entry id 设置为结果。将读取的字节数增加到 consumed id string bytes。
-
通过以 url template 和 entry id 作为输入调用扩展 URL 模板,将 entry id 转换为 URL 字符串。如果模板扩展 产生错误,则返回错误。
-
将生成的补丁 URL 字符串添加到 entry。
-
如果读取的长度值的最高有效位被设置,则重复步骤 8。
-
-
-
如果formatFlags的位 2 未被设置:
-
如果不存在 id string bytes,则:
-
将 entry id 设置为 entry id + 1。
-
通过以 url template 和 entry id 作为输入调用扩展 URL 模板,将 entry id 转换为 URL 字符串。如果模板扩展 产生错误,则返回错误。
-
将生成的补丁 URL 字符串添加到 entry。
-
-
否则,如果存在 id string bytes,则:
-
通过以 url template 和 entry id 作为输入调用扩展 URL 模板,将 entry id 转换为 URL 字符串。如果模板扩展 产生错误,则返回错误。
-
将生成的补丁 URL 字符串添加到 entry。
-
-
-
如果formatFlags的位 3 被设置,则存在补丁格式。 从 entry bytes 读取patchFormat指定的格式,并将 entry 的补丁格式设置为读取值。 如果patchFormat不是§ 6.1 格式摘要中的值之一,则此编码无效, 返回错误。
-
如果formatFlags的位 4 和位 5 中一个或两个被设置,则存在码位 列表:
-
如果formatFlags的位 4 为 0 且位 5 为 1,则 从 entry bytes 中读取 2 字节(uint16)的bias值。
-
如果formatFlags的位 4 为 1 且位 5 为 1,则 从 entry bytes 中读取 3 字节(uint24)的bias值。
-
否则,bias 为 0。
-
按照§ 5.3.2.3 稀疏位集合, 从 entry bytes 中读取带 bias 的稀疏位集合codePoints。 将得到的码位集合添加到 entry 中第一个字体子集 定义中。如果稀疏位集合解码 失败,则此编码无效,返回错误。
-
-
如果formatFlags的位 6 被设置,则将 ignored 设为 true。否则 ignored 为 false。
-
返回 entry id、entry、consumed bytes、consumed id string bytes 和 ignored。
5.3.2.2. 从格式 2 中移除条目
此算法用于从格式 2 补丁映射中移除条目。此移除操作会修改 补丁映射的字节,但不会 改变字节数量。
从格式 2 补丁映射中移除条目
该算法的输入为:
-
patch map:一个使用格式 2 补丁映射编码的补丁映射。此过程可修改 该补丁映射。
-
patch URL string:标识要移除条目的补丁 URL 字符串。
此算法是解释格式 2 补丁映射的修改版本。 使用 patch map 作为输入调用解释格式 2 补丁映射,但进行以下更改:
-
在解释格式 2 补丁映射 条目的步骤 8 和 9 期间:仅将为第一个增量/长度值生成的 URL 字符串与 patch URL string 进行比较;如果两者相等,则将formatFlags的位 6 设为 1。 此时停止解释过程。
-
不使用解释格式 2 补丁映射的 返回值。
5.3.2.3. 稀疏位 集合
稀疏位集合是一种紧凑存储一组不同无符号整数的数据结构。该集合 表示为一棵树,其中 每个节点具有固定数量的子节点,这些子节点递归地将一个区间细分为相等分区。高度为 H、 分支因子为 B 的树,可以存储区间 [0 到 BH-1](包含端点)中各整数是否属于集合。该树 被编码为字节数组以便传输。
在格式 2 补丁映射的上下文中,稀疏位集合用于存储一组Unicode码位。因此, 存储在稀疏位集合中的整数 值仅限于 0 到 0x10FFFF 范围内的Unicode码位值。
稀疏位集合编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint8 | header | 位 0(最低有效位)和位 1 通过分支因子编码对树的分支因子 B 进行编码。位 2 到位 6 是一个 5 位无符号整数,用于编码 H 的值。位 7 设为 0,保留供 未来使用。 |
| uint8 | treeData[variable] | 树的二进制编码。 |
treeData 的确切长度起初未知,其长度通过执行 解码算法确定。使用 分支因子 2 或 4 时,最后一个节点可能只使用一个字节中的部分位。在这种情况下,所有 剩余位都未使用并被 忽略。
分支因子 编码:
| 位 1 | 位 0 | 分支因子(B) | 最大高度(H) |
|---|---|---|---|
| 0 | 0 | 2 | 31 |
| 0 | 1 | 4 | 16 |
| 1 | 0 | 8 | 11 |
| 1 | 1 | 32 | 7 |
编码高度(H)大于上表中相应编码 分支因子(B)最大高度的稀疏位集合 无效。
解码稀疏位集合 treeData
该算法的输入为:
-
treeData:要解码的字节数组。
-
bias:添加到每个解码后集合成员的无符号整数值。
该算法输出:
-
S:无符号整数集合。
该算法使用一个 FIFO(先进先出)队列 Q:
-
从 treeData 中移除第一个字节。该字节为头部字节。按照稀疏位集合确定树高度 H 和 分支因子 B。
-
如果 H 大于分支因子编码 表中 B 所在行的“最大高度”, 则编码无效,返回错误。
-
如果 H 等于 0,则这是空集合,并且不会继续消耗 treeData 的任何字节。 返回空集合。
-
将元组 (0, 1) 插入 Q。
-
将 S 初始化为空集合。
-
treeData 被解释为位字符串,其中 treeData[0] 的最低有效位是 字符串中的第一个位,treeData[0] 的最高有效位是第 8 位,依此类推。
-
如果在以下步骤中添加到 S 的值大于最大 Unicode 码位值(0x10FFFF),则 忽略该值,不将其添加到 S。
-
如果 Q 为空,则返回 S。
-
从 Q 中取出下一个元组 t。t 中的第一个值 是 start,第二个值是 depth。
-
从 treeData 位字符串中移除接下来的 B 位。第一个移除的位是 v1, 第二个是 v2,依此类推,最后一个移除的位是 vB。如果移除前 treeData 中剩余位少于 B,则 treeData 格式错误, 返回错误。
-
如果 v1 到 vB 的所有位都为 0,则将 区间 [start + bias, start + bias + BH - depth + 1) 中的所有整数插入 S。转到步骤 5。
-
对于 v1 到 vB 中每个等于 1 的 vi: 如果 depth 等于 H,则将 整数 start + bias + i - 1 添加到 S。否则,将元组 (start + (i - 1) * BH - depth, depth + 1) 插入 Q。
-
转到步骤 8。
注:编码稀疏位集合时,编码器可以使用 任何可用分支因子,但建议 使用 4,因为已有研究表明, 对于通常遇到的大多数 Unicode 码位集合,它可以产生最小的编码。
位字符串: |-- 头部 --|- 0 级 |---- 1 级 ----|------- 2 级 -----------| | B=8 H=3 | n0 | n1 n2 | n3 n4 n5 | [ 01 11000 0 10000100 10001000 10000000 00100000 01000000 00010000 ] 随后变为以下字节字符串: [ 0b00001110, 0b00100001, 0b00010001, 0b00000001, 0b00000100, 0b00000010, 0b00001000 ]
位字符串: |-- 头部 -- | | B=2 H=0 | [ 00 00000 0 ] 随后变为以下字节字符串: [ 0b00000000, ]
位字符串: |-- 头部 --| 0 级 |- 1 级 -| 2 级 | | B=4 H=3 | n0 | n1 | n2 | n3 | [ 10 11000 0 1100 0000 1000 1100 ] 字节字符串: [ 0b00001101, 0b00000011, 0b00110001 ]
5.3.3. URL 模板
URL 模板用于压缩与每个补丁映射条目关联的 URL 字符串。每个补丁映射 表 提供一个 URL 模板,随后通过使用每个条目的 ID 作为输入扩展该公共模板, 生成每个条目的 URL 字符串。 模板扩展的输出是使用 [UTF-8] 编码的字符串。
URL 模板编码为一系列单字节操作码,这些操作码可插入字面值, 或根据输入条目 ID 插入值。
下面给出解码和扩展 URL 模板的算法。
扩展 URL 模板
该算法的输入为:
-
url template bytes:包含 URL 模板的字节数组。
-
entry ID:条目的 ID。可以是无符号整数或字节数组。
该算法输出:
-
URL string:表示 URL 字符串的字节数组。使用[UTF-8]编码。
算法如下:
-
将 URL string 初始化为空字节数组。在其余步骤中,将 url template bytes 用作 队列。 读取操作从队列前端移除字节(从索引 0 处的字节开始)。
-
将 url template bytes 的下一个字节读取到 op code 中。如果没有 剩余字节,则 扩展完成,返回 URL string。
-
如果 op code 的最高有效位未被设置,则:
-
将 op code 的最低 7 位解释为无符号整数 number of literals。
-
如果 number of literals 为 0,则返回错误。
-
从 url template bytes 中读取接下来的 number of literals 个字节到 literal bytes。 如果 url template bytes 中剩余字节不足,则返回 错误。
-
按照 UTF-8,ISO 10646 的一种转换格式 § section-4,检查 literal bytes 是否为有效的 UTF-8 编码字符串。 如果不是,则返回错误。
-
将 literal bytes 追加到 URL string 末尾。
-
转到步骤 2。
-
-
如果 op code 的最高有效位被设置,则:
-
在后面的操作码参考表中查找 op code。执行 “插入操作”列中指定的操作。如果表中找不到 op code,则 返回错误。
-
转到步骤 2。
-
操作码参考表
| 名称 | 操作码值 | 插入操作 |
|---|---|---|
| 插入 id32 | 128 (0b10000000) | 将后面的扩展变量表中 id32 变量的值追加到 URL string。 |
| 插入 d1 | 129 (0b10000001) | 将后面的扩展变量表中 d1 变量的值追加到 URL string。 |
| 插入 d2 | 130 (0b10000010) | 将后面的扩展变量表中 d2 变量的值追加到 URL string。 |
| 插入 d3 | 131 (0b10000011) | 将后面的扩展变量表中 d3 变量的值追加到 URL string。 |
| 插入 d4 | 132 (0b10000100) | 将后面的扩展变量表中 d4 变量的值追加到 URL string。 |
| 插入 id64 | 133 (0b10000101) | 将后面的扩展变量表中 id64 变量的值追加到 URL string。 |
扩展变量
| 变量 | 值 |
|---|---|
| id32 | 输入的 entry ID 编码为base32hex字符串(使用 数字 0-9、A-V),并省略填充。当 entry ID 是无符号整数时,必须先将其转换为大端 32 位 无符号整数, 但随后要在编码前移除所有等于 0 的前导字节。(例如,当 整数小于 256 时,只编码一个字节。)如果 entry ID 为 0,则编码一个零 字节。当 entry ID 为字符串时,原始字节使用 base32hex 编码。 |
| d1 | id32 变量中字符串的最后一个字符。 如果 id32 变量为空,则值为字符 _(U+005F)。 |
| d2 | id32 变量中字符串的倒数第二个字符。 如果 id32 变量少于 2 个字符,则值为字符 _ (U+005F)。 |
| d3 | id32 变量中字符串的倒数第三个字符。 如果 id32 变量少于 3 个字符,则值为字符 _ (U+005F)。 |
| d4 | id32 变量中字符串的倒数第四个字符。 如果 id32 变量少于 4 个字符,则值为字符 _ (U+005F)。 |
| id64 | 输入的 entry ID 编码为base64url字符串(使用 字符 A-Z、a-z、0-9、- (减号)和 _(下划线)),并包含填充。由于填充字符为 '=',必须 将其 URL 编码为 '%3D'。当 entry ID 是无符号整数时,必须 先将其转换为大 端 32 位无符号整数,但随后要在编码前移除所有等于 0 的前导 字节。(例如,当整数小于 256 时,只编码一个字节。) 如果 entry ID 为 0,则编码一个 零字节。当 entry ID 为字符串时,其原始字节编码为base64url。 |
以下是一些示例输入及其对应的扩展结果。“模板字节”列中的值使用 C 风格字节数组表示。
| 模板字节 | 输入 ID | ID 类型 | 扩展结果 |
|---|---|---|---|
| [16, 'h', 't', 't', 'p', 's', ':', '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 128] | 123 | 整数 | https://foo.bar/FC |
| [8, 'f', 'o', 'o', '?', 'b', 'a', 'r', '=', 128] | 123 | 整数 | foo?bar=FC |
| [10, '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 128] | 0 | 整数 | //foo.bar/00 |
| [5, '/', 'f', 'o', 'o', '/', 129, 1, '/', 130, 1, '/', 128] | 478 | 整数 | /foo/0/F/07F0 |
| [5, '/', 'f', 'o', 'o', '/', 129, 1, '/', 130, 1, '/', 131, 1, '/', 128] | 123 | 整数 | /foo/C/F/_/FC |
| [4, 'f', 'o', 'o', '/', 129, 1, '/', 130, 1, '/', 131, 1, '/', 128] | ['b', 'a', 'z'] | 字符串 | foo/K/N/G/C9GNK |
| [4, 'f', 'o', 'o', '/', 129, 1, '/', 130, 1, '/', 131, 1, '/', 128] | ['z'] | 字符串 | foo/8/F/_/F8 |
| [10, '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 133] | 14,000,000 | 整数 | //foo.bar/1Z-A |
| [10, '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 133] | 0 | 整数 | //foo.bar/AA%3D%3D |
| [10, '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 133] | 17,000,000 | 整数 | //foo.bar/AQNmQA%3D%3D |
| [10, '/', '/', 'f', 'o', 'o', '.', 'b', 'a', 'r', '/', 133] | [0xc3, 0xa0, 0x62, 0x63] | 字符串 | //foo.bar/w6BiYw%3D%3D |
在以下示例中,扩展预计会失败并返回错误:
| 模板字节 | 输入 ID | ID 类型 | 失败原因 |
|---|---|---|---|
| [4, 'f', 'o', 'o', '/', 150] | 123 | 整数 | 操作码 150 无效 |
| [4, 'f', 'o', 'o', '/', 0, 128] | 123 | 整数 | 操作码 0 无效 |
| [10, 'f', 'o', 'o', '/', 128] | 123 | 整数 | 模板中没有足够字节供字面值复制操作码 10 使用。 |
| [4, 'f', 'o', 'o', 0x85, 128] | 123 | 整数 | 字面值字节不是有效的 UTF-8。 |
6. 字体补丁格式
在增量字体传输中,通过应用补丁扩展字体 子集。 本规范定义两种补丁格式,每种格式分别适用于一组不同的扩充场景。单个 编码可以使用多种补丁格式。
6.1. 格式摘要
本规范定义以下补丁格式:
-
§ 6.2 以表为键:一组使用 字体子集中的表作为基础的、 使用 brotli 编码的二进制差异。
-
§ 6.3 以字形为键:一组不透明二进制数据块,每个数据块 都与一个字形 id 和表关联。
以下各节中给出了每种算法的更详细描述。
以下格式编号用于在§ 5.3 补丁映射表中标识补丁格式和失效模式:
| 格式编号 | 名称 | 失效模式 |
|---|---|---|
| 1 | § 6.2 以表为键 | 完全 失效 |
| 2 | § 6.2 以表为键 | 部分 失效 |
| 3 | § 6.3 以字形为键 | 不失效 |
6.2. 以表为键
以表为键的补丁包含一组应用于输入字体文件中各个字体表 的补丁。每个表补丁使用brotli 压缩进行编码,并使用输入字体文件中的对应表作为共享 LZ77 字典。以表为键的编码补丁由一个短头部及其后的 一个或多个 brotli 编码补丁组成。除了修补表之外,补丁还可以替换(不使用现有 表数据) 或移除字体 子集中的表。
以表为键的补丁 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| Tag | format | 标识 格式为以表为键,必须设为 'iftk' |
| uint32 | reserved | 保留供未来使用,设为 0。 |
| uint32 | compatibilityId[4] | 可应用此补丁的字体 子集的 id。参见§ 4.1 补丁失效。 |
| uint16 | patchesCount | patches 数组中的条目数量。 |
| Offset32 | patches[patchesCount+1] | 每个条目都是从此表起始位置到某个TablePatch的偏移。偏移必须 按升序排序。 |
patches数组中两个连续偏移之差,给出 对应TablePatch的 大小。
TablePatch 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| Tag | tag | 标识此补丁所应用的字体 表的标签。 |
| uint8 | flags | 位字段。如果设置位 0(最低有效位),则此补丁替换现有表。如果 设置位 1,则移除此表。 |
| uint32 | maxUncompressedLength | brotliStream的最大未压缩长度。 |
| uint8 | brotliStream[variable] | Brotli 编码的字节流。 |
6.2.1. 应用 以表为键的补丁
此补丁应用算法用于应用以表为键的 补丁,以扩展字体子集, 使其覆盖更多码位、 特性和/或设计变体空间。
应用以表为键的补丁
该算法的输入为:
-
base font subset:要扩展的字体子集。
-
patch:要应用到 base font subset 的以表为键的补丁。
-
compatibility id:base font subset 中列出此补丁的 'IFT ' 或 'IFTX' 表中的 ID 编号。
该算法输出:
-
extended font subset:由 patch 扩展后的字体子集。
算法如下:
-
将 extended font subset 初始化为不含任何表的空字体。
-
检查 patch 是否符合§ 6.2 以表为键中的要求(要求使用 “必须”标记),并检查所有TablePatch 是否都包含在 patch 内。否则返回错误。
-
检查 patch 中的compatibilityId字段是否 等于 compatibility id。 如果不匹配,或 base font subset 没有 'IFT ' 或 'IFTX' 表, 则补丁应用 失败,返回错误。
-
在以下步骤中,向 extended font subset 添加表,是指将 表的数据添加到字体中,并 按照 OpenType 规范的要求,向表 目录插入一个新条目。该条目包含表数据的校验和。复制未修改的现有表时,客户端 可以重新使用源字体条目中的校验和。否则需要 计算新的校验和。
-
对于patches中的每个条目(索引为 i):
-
找到与索引 i 关联的TablePatch。 对象从偏移patches[i] (包含)开始,在偏移patches[i+1](不包含)结束。两个偏移都 相对于 patch 的起始位置。
-
如果此前已应用patches中的某个条目,且其tag与 此条目相同,则忽略此条目并继续迭代下一个条目。条目按照它们在 patches数组中的顺序处理。
-
如果flags的位 1 被设置,则不要将由tag标识的表 复制或添加到 extended font subset。继续处理下一个条目。
-
如果flags的位 0(最低有效位)被设置,则按照 Brotli 压缩数据格式 § 10 解码算法解码brotliStream。 不使用共享字典。如果解码后的数据大于maxUncompressedLength,则返回错误。 如果brotliStream中存在任何未被解码过程使用的数据, 则返回错误。 向 extended font subset 添加一个由tag标识的表, 并将其内容设置为解码后的brotliStream。继续处理下一个条目。
-
否则,按照Brotli 压缩数据 格式 § 10 解码算法解码brotliStream,并使用 base font subset 中由 tag标识的表 作为共享 LZ77 字典。如果不存在这样的表,则返回错误。如果解码后的数据 大于maxUncompressedLength,则返回 错误。如果brotliStream中存在任何 未被解码过程使用的数据,则返回错误。向 extended font subset 添加一个由tag标识的表, 并将其内容设置为解码后的brotliStream。
-
-
对于 base font subset 中每个标签未在步骤 5 处理的任何 条目中出现的表, 将该表的副本添加到 extended font subset。
6.3. 以字形为键
以字形为键的补丁包含一组数据块,每个数据块都与一个字形索引和一个字体表关联。 编码数据会替换所引用表中 该字形索引的任何现有数据。以字形为键的补丁可以对glyf/loca、gvar、CFF 和 CFF2 表的数据进行编码。
以字形为键的补丁 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| Tag | format | 标识格式为以字形为键, 必须设为 'ifgk' |
| uint32 | reserved | 保留供未来使用,设为 0。 |
| uint8 | flags | 位字段。如果设置了位 0(最低有效位),则glyphIds使用 uint24, 否则使用 uint16。 |
| uint32 | compatibilityId[4] | 可应用此补丁的字体子集的兼容性 id。参见§ 4.1 补丁失效。 |
| uint32 | maxUncompressedLength | brotliStream的最大未压缩长度。 |
| uint8 | brotliStream[variable] | Brotli 编码的GlyphPatches表。 |
GlyphPatches 编码:
| 类型 | 名称 | 描述 |
|---|---|---|
| uint32 | glyphCount | 补丁中编码的字形数量。 |
| uint8 | tableCount | 补丁包含数据的表数量。 |
| uint16/uint24 | glyphIds[glyphCount] | 补丁中包含的字形索引数组。如果flags的位 0(最低 有效位)被设置,则元素为 uint24,否则元素为 uint16。 必须按 升序排列,并且不得 包含任何重复值。 |
| Tag | tables[tableCount] | 补丁中包含的表 (按标签)的数组。必须按升序 排列,并且不得包含任何重复值。排序时,标签值 解释为 4 字节大端 无符号整数,并按整数值排序。 |
| Offset32 | glyphDataOffsets[glyphCount * tableCount + 1] | 每个表中各字形数据偏移的数组。前glyphCount个偏移 对应tables[0],接下来的glyphCount个偏移 (如果存在)对应tables[1],依此类推。所有偏移都相对于 GlyphPatches 表的起始位置。偏移必须按 升序排序。 |
| uint8 | glyphData[variable] | 由偏移选出的实际字形数据。 |
glyphDataOffsets 数组中两个连续偏移之差,给出 对应字形数据的大小。
6.3.1. 应用 以字形为键的补丁
此补丁应用算法用于应用以字形为键的 补丁,以扩展字体子集, 使其覆盖更多码位、 特性和/或设计变体空间。
应用以字形为键的补丁
该算法的输入为:
-
base font subset:要扩展的字体子集。
-
patch:要应用到 base font subset 的以字形为键的补丁。
-
patch URL string:补丁数据所在位置的 URL 字符串。
-
compatibility id:base font subset 中列出此补丁的 'IFT ' 或 'IFTX' 表中的兼容性 ID。
该算法输出:
-
extended font subset:由 patch 扩展后的字体子集。
算法如下:
-
检查 patch 是否符合§ 6.3 以字形为键中的要求(要求使用 “必须”标记)。否则返回错误。
-
检查 patch 中的compatibilityId字段是否 等于 compatibility id。 如果不匹配,或 base font subset 没有 'IFT ' 或 'IFTX' 表, 则补丁应用 失败,返回错误。
-
按照Brotli 压缩数据 格式 § 10 解码算法,解码brotliStream中的 brotli 编码数据。解码后的 数据是一个GlyphPatches表。如果解码后的数据大于maxUncompressedLength,则返回 错误。
-
-
使用 base font subset 中的对应表合成 一个新表,其中每个字形的数据,如果glyphData中存在该字形索引对应的数据, 就用该数据替换;否则,从 base font subset 中的对应表复制 该字形索引的数据。
-
通过寻找等于该字形索引的glyphIds[j], 定位某个字形索引的补丁字形数据。关联字形数据的 偏移为glyphDataOffsets[i * glyphCount + j]。 关联字形数据的长度 等于glyphDataOffsets[i * glyphCount + j + 1] 减去glyphDataOffsets[i * glyphCount + j]。
-
合成新表的具体过程取决于指定的 表格式。 任何与字形无关的数据都应从 base font subset 中的表复制。支持 glyf、gvar、CFF 或 CFF2 类型的表。 必须忽略 任何其他类型表的条目。更新glyf时, 还必须更新loca 表。此步骤不得导致修改字体中的任何其他表。 特别是,这意味着补丁不能添加索引超出maxp中所指定 numGlyphs 的字形。
CFF、CFF2 和 gvar 对每个字形数据使用 可变大小的偏移。如果 合成新表时有必要,可以按需增加偏移大小。对于glyf/loca,偏移 大小不能更改,因为偏移大小在head表中指定。 如果合成期间生成的偏移 超过最大可能偏移值(在可能的情况下增加偏移大小之后),则补丁应用 失败。返回错误。
-
如果 base font subset 没有匹配的表,则返回错误。
-
将合成的表插入 extended font subset。
-
-
找到与 compatibility id 具有相同compatibilityId的§ 5.3 补丁映射表。 如果它是 格式 1 补丁映射,则以补丁映射表和 patch URL string 作为 输入,调用从格式 1 补丁映射中移除条目。否则,如果它是格式 2 补丁映射,则以补丁映射表和 patch URL string 作为输入,调用从格式 2 补丁映射中移除条目。将修改后的补丁映射表复制到 extended font subset 中。
-
对于 base font subset 中每个标签未在步骤 4 或 5 处理的任何 条目中出现的表, 将该表的副本添加到 extended font subset。
7. 编码
编码器是一种生成增量字体及一组关联补丁的工具。“编码”是指使用编码器的 过程,包括编码器要求或允许影响特定 情况下结果的任何参数。 合规编码器生成的增量字体 和关联补丁:
-
必须满足 § 5 字体格式扩展和§ 6 字体补丁格式中的所有要求。
-
必须保持一致,即: 对于任何可能的字体子集定义,使用该子集定义和增量字体调用扩展增量字体 子集所得的结果必须始终相同, 无论在扩展增量字体 子集的步骤 8 中选择了何种具体的补丁选择顺序。
-
必须 遵守补丁失效条件。作为 IFT 编码一部分的任何补丁, 在应用到兼容的字体子集时,只能对补丁映射兼容性 ID 进行符合§ 4.1 补丁 失效条件的更改, 这些条件由关联补丁映射条目所声明的失效模式决定。
-
当编码器用于将现有字体转换为增量字体时,关联的完全扩展字体应与现有字体 等效。等效的完全扩展字体 应具有与现有字体相同的所有表 (不包括增量 IFT/IFTX 表),且每个表都应在功能上等效于现有字体中的对应表。注:完全 扩展后的字体不一定始终与现有字体的二进制内容完全匹配。
-
应在整个扩充过程中保留完全扩展字体的功能,即: 给定从增量 字体派生的完全扩展字体以及任意内容,则使用增量字体和覆盖该内容的最小子集定义调用扩展增量字体 子集生成的字体子集,对于该内容应与完全扩展字体的 渲染结果相同。
当编码器用于将现有字体文件转换为增量字体,并且客户端按照 本文档其他各节实现时,IFT 规范的目标是使字体在客户端中的外观和行为 与整个文件都传输到客户端时相同。IFT 规范的主要目标之一,是使 IFT 格式和协议可以像 WOFF2 一样,充当字体传输的中立媒介。如果编码器从源字体生成 满足上述全部要求(1 到 5)的编码,则该编码将保留原始字体的全部 功能。上面的要求 4 确保原始字体中的所有功能都可达。该要求与要求 5 配合使用,后者要求 IFT 字体的部分版本对于属于生成该部分字体所用子集定义的内容, 具有与完整版本(此处为原始字体)等效的功能。
这对于字体铸造厂或字体的其他权利所有者可能很重要,他们希望确信 使用 IFT 对该 字体进行编码和传输不会改变其行为,从而不会改变字体创作者的意图。许可证或 合同随后可能包含 有关 IFT 一致性的要求;而在某些情况下,由于 WOFF2 的内容中立性, 将字体重新编码为 WOFF2 格式事实上是允许的,这些情况也可能允许对该字体进行 IFT 编码。
但是,这些编码一致性要求并不意味着排除或弃用 不保留源字体全部功能的 编码的可能性及其实际用途。任何满足最低 要求(上述 1、2 和 3)的编码都是有效的,并可能有适当用途。在某些情况下,可能希望编码后的字体 在其所有补丁文件中省略对某些 功能/数据的支持,即使原始字体文件包含这些内容。在 其他情况下,字体可能直接从字体创作源文件编码为 IFT 格式。如果编码器选择不 满足上述要求 4, 仍强烈建议满足要求 5,因为它可以确保字体在整个 扩充过程中的行为保持一致。
7.1. 编码注意事项
本节为非规范性内容。
编码过程的细节可能因编码器而异,超出本文档的范围。但是, 本节提供 编码器实现可能需要考虑的指导,并且在生成现有字体文件的增量版本时,这些指导对于重现 该字体文件的外观和行为可能很重要。本节提供的指导 基于在制定本规范期间构建编码器实现的经验。 它代表了截至本文撰写时,对如何生成高性能编码的最佳理解, 该编码满足 § 7 编码的要求 1 到 4,因而保留被编码原始字体的所有 功能/行为。
关于§ 6.2 以表为键补丁
§ 6.2 以表为键补丁可以更改某些字体表的内容,而不 更改其他表。每个被修补的表通常需要 相对于特定表内容,但其他表可以具有不同内容。因此,只要 § 6.2 以表为键补丁不改变包含字形数据的表,它就可以 与§ 6.3 以字形为键补丁兼容,因此仅为部分 失效(即它会使其他§ 6.2 以表为键补丁失效, 但不会使§ 6.3 以字形为键补丁失效)。此外,如果两组§ 6.2 以表为键补丁不 修改任何相同的表,则它们可以彼此独立。例如,可以将§ 6.2 以表为键 补丁用于字形表之外的所有 内容,然后为这些表使用另一组§ 6.2 以表为键 补丁,而不是§ 6.3 以字形为键补丁;理论上,这两组都可以是部分失效的——使它们 各自内部相互依赖,但彼此独立。
应用§ 6.2 以表为键补丁通常会修改列出它的 IFT 或 IFTX 表,以添加一组新的 补丁,进一步扩展字体。这意味着全部§ 6.2 以表为键补丁形成一张图, 其中分段中的每个字体子集都是节点,每个补丁都是边。这也意味着此类 补丁通常会串行下载和应用,这对该补丁 类型相对于延迟的性能有影响。
关于§ 6.3 以字形为键补丁
§ 6.3 以字形为键补丁与其他补丁类型有很大不同。第一, § 6.3 以字形为键补丁只能修改 包含字形轮廓数据的表,因此仅使用增量字体中的§ 6.3 以字形 为键补丁时,必须在初始字体文件中包含 所有其他字体表数据。第二,§ 6.3 以字形为键补丁不会使其他补丁 失效, 因此可以独立下载和应用。这种独立性意味着可以并行 下载多个补丁, 与失效补丁类型相比,可显著减少所需的往返次数。
为编码选择补丁格式
所有编码都必须选择一种或多种补丁类型。§ 6.2 以表为键补丁 允许修补字体中的 所有类型数据,但由于此类型至少是部分失效的, 所需补丁总数会随片段数量呈指数增长,而不是线性增长。 § 6.3 以字形为键补丁 只能更新轮廓和变体增量数据,但所需数量随片段数量线性增长。
除了补丁数量之外,编码器还应考虑获取典型内容所需补丁时 需要的网络往返次数。对于失效补丁类型,必须串行发出补丁请求。 这意味着,如果某些 内容需要多个片段,则可能需要多次网络往返。另一方面,以字形为键的补丁不会 使其他补丁失效,并且可以并行获取,只需一次往返。
在这两种类型的极端情况下,§ 6.2 以表为键补丁最适合 非轮廓数据量很大但只需要 少量补丁的字体。§ 6.3 以字形为键补丁最适合 绝大多数数据由字形 轮廓组成的字体,许多现有 CJK 字体都属于这种情况。
对于介于二者之间的字体,或希望对字形数据进行细粒度分段、但 其他表中的数据 仍需分段的情况,可以采用以下方式混合§ 6.2 以表为键 和§ 6.3 以字形为键补丁类型:
-
将所有以表为键的补丁条目保留在一个映射表中,将所有以字形为键的条目保留在另一个 映射表中。
-
使用以表为键的补丁更新除以字形为键的补丁会涉及的表之外的所有表 (轮廓、 变体增量和以字形为键的补丁映射表)。这些补丁应使用少量 大片段,以保持 补丁数量合理。
-
由于以字形为键的补丁引用被更新的特定字形 ID,以表为键的 补丁不得更改 原始字体中使用的字形到字形 ID 的分配;否则,以字形为键的补丁中列出的字形 ID 可能会 变得不正确。在字体子集化工具中,这通常作为“保留字形 ID”选项提供。
-
最后,使用以字形为键的补丁更新剩余的表;此时可使用更小、更细粒度的 片段,而不会 需要过多补丁。
预计混合补丁类型编码将是一种常用方法,因为许多字体都会位于这两个 极端之间。
使用失效补丁减少往返次数
对于使用某种失效补丁类型的分段,减少所需往返次数的一种方法,是提供 可一次添加多个片段的 补丁(除单片段补丁之外)。例如,考虑一个具有 4 个片段的字体: A、B、C 和 D。补丁表可以列出添加以下内容的补丁:A、B、C、D、A + B、A + C、A + D、B + C、B + D 和 C + D。这样 可以在一次往返中添加任意两个片段。该方法的缺点是会进一步 增加所需唯一 补丁的数量。
编码补丁映射中的条目顺序
在§ 4.4 选择失效补丁中,客户端使用补丁映射中 条目的顺序,在选择加载和应用哪个补丁时打破平局。 客户端的目标是减少总传输大小,因此当多个条目的交集大小相同时, 通常选择总传输大小最小的补丁最符合客户端利益。因此,编码器应按字节大小 从小到大排列补丁映射中的条目。这样可以确保客户端遵循§ 4.4 选择失效补丁时,偏向较小补丁并最大化性能。
管理补丁数量
将§ 6.2 以表为键补丁与大量片段结合使用,可能导致 需要的补丁数量非常庞大,这可能产生两个 负面影响。第一,存储所有预生成补丁所需的空间可能大得不可接受。 第二,更多 补丁通常意味着更低的 CDN 缓存性能,因为更多补丁代表 从给定子集到给定子集的更多路径,而不同用户会根据其访问的 内容采用不同路径。 可以使用一些技术减少预生成补丁总数:
-
为补丁图设置最大深度;达到该限制后,修补字体以添加完整 原始字体的所有剩余部分。这会导致在添加一定数量的 片段后加载整个剩余字体。限制 图的深度可减少较深层级的补丁组合爆炸。
-
或者,在较深层级,编码器可以开始将多个片段合并为单个 补丁,以减少每一层的分支数。
选择分段方式
编码器需要做出的最重要、最复杂的决定之一,是如何对 编码字体中的数据进行分段。上面的讨论 主要关注片段数量,但增量字体的性能更多取决于 片段内数据的 分组方式。为最大化效率,编码器需要将通常一起使用的数据(例如码位) 分组到同一 片段中。这样可减少客户端扩展字体时加载的不必要数据量。编码器 还必须确定片段大小。较小片段会产生更多补丁,从而因需要更多 网络请求而带来更多开销,但与较大片段相比, 通常每个片段中包含的不必要数据更少。对码位进行分段时,码位使用 频率数据有助于指导分段。
某些码位可能具有显而易见的分段方式,或者至少对于应将哪些 码位分组在一起几乎没有疑问。 例如,拉丁字母表中的大写和小写字母构成一个自然分组。其他情况可能更加 复杂。例如, 中文、日文和韩文共享一些码位,但在日文中高频的码位 可能在中文中频率较低。 在某些情况下,可以选择针对单一语言优化编码。另一种方法是生成折中 编码。例如,在分段时,编码器可以把日文、 中文和韩文中都高频的码位放入一个 片段,然后把仅在日文和中文中高频的码位放入另一个片段,依此类推。之后, 仅在一种语言中高频的码位可以按通常方式处理。这样会导致片段 大小不那么均匀,但意味着 为任一种语言加载高频补丁时,不会同时加载低频字形。
包含默认布局特性
附录 A:默认特性标签汇总了一组通常默认使用的布局特性。 由于此列表中的特性 通常始终会被塑形器使用,因此为获得最佳性能,编码器通常不应在字体编码中 将这些特性设为可选。
保持功能等效
如§ 7 编码中所述,编码器应保留 原始字体的功能。字体非常复杂, 并且码位之间经常存在交互,因此使用字体的部分副本保持功能等效可能很棘手。 接下来的两个小节讨论如何使用不同补丁类型保持功能等效。
以表为键的补丁
在准备§ 6.2 以表为键补丁时,实现功能 等效的一种方法,是利用 现有字体子集化工具实现生成保留原始字体功能的字体子集。随后可从这些子集 派生 IFT 补丁。
字体子集化工具根据所需的字体子集定义,从输入字体生成字体 子集。可靠地 对字体进行子集化的实践已得到充分理解,并存在多个开源实现(完整的形式化描述 超出本文档范围)。它通常涉及可达性分析,其中相对于字体子集定义检查表中 的数据, 以确定哪些部分可由子集定义覆盖的任何可能内容访问。 任何可达数据都会保留在生成的字体子集中,而任何不可达数据都可以移除。
在以下示例伪代码中,字体子集化工具用于生成仅使用 以表为键补丁的 IFT 编码字体:
# 将字体(full_font)编码为一个从 base_subset_def 开始的增量字体, # 并且可以增量添加 subset_definitions 中的任意定义。返回 IFT 编码字体 # 以及一组关联补丁。 encode_as_ift ( full_font , base_subset_def , subset_definitions ): base_font = subset ( full_font , base_subset_def ) base_font , patches = encode_node ( full_font , base_font , base_subset_def , subset_definitions ) return base_font , patches # 更新 base_font,添加所有 IFT 补丁映射,以到达 # subset_definitions 中的任意定义,并生成关联补丁。 encode_node ( full_font , base_font , cur_def , subset_definitions ): patches = [] next_fonts = [] for each subset_def in subset_definitions not fully covered by cur_def : next_def = subset_def union cur_def next_font = subset ( full_font , next_def ) let patch_url be a new unique url add a mapping from , ( subset_def - cur_def ) to patch_url , into base_font next_font , patches += encode_node ( full_font , next_font , next_def , subset_definitions ) next_fonts += ( next_font , next_def , patch_url ) for each ( next_font , next_def , patch_url ) in next_fonts : patch = table_keyed_patch_diff ( base_font , next_font ) patches += ( patch , patch_url ) return base_font , patches
在此示例实现中,如果输入的基础子集定义与子集 定义列表的并集完全覆盖输入的 完整字体,并且所使用的子集化工具实现正确保留所有功能,则上述 实现应满足 § 7 编码中作为中立编码的要求。此基本编码器 实现仅用于演示,不代表 所有可能的编码器实现。特别是,它没有使用或 展示以字形为键补丁的用法。大多数编码器可能会更复杂,并需要考虑更多因素,其中一些将在 后续 各节中讨论。
正因为它们以码位和特性标签为参数,但又可以彼此独立 应用,§ 6.3 以字形为键补丁有额外要求,不能 直接通过使用子集化工具实现来派生。但是, 这类实现有助于澄清编码器在生成这种补丁时,为保持功能等效需要执行什么操作。 考虑相对于给定字体子集定义生成字体子集的结果。我们可以 将该字体 子集定义的字形闭包 定义为子集中包含的完整字形集合,子集化工具已确定这些字形 是渲染所描述码位和布局特性的任意组合所必需的。
使用该定义,完整§ 6.3 以字形为键补丁集合的字形闭包要求为:
假设子集化工具准确完成其工作,则字形闭包要求是等效 行为要求的结果:假设存在一个字体子集定义,子集化工具在其子集中包含字形 *i*,但生成§ 6.3 以字形为键补丁的编码器从与该定义对应的补丁集合中 省略了字形 *i*。如果子集化工具是正确的, 则渲染该定义中码位和特性的某种组合时,必须存在该字形才能保持等效行为, 这意味着增量字体在渲染该 组合时不会具有等效行为。
因此,在生成使用以字形为键补丁的编码时,编码器必须确定如何 在所有补丁之间分配字形, 以满足字形闭包要求。这主要涉及查看分配给 某个片段的码位,并确定该片段对应的补丁中还必须包含哪些其他字形,例如当 某个字形变体可 替换片段中按码位包含的字形时。在某些情况下,只有加载多个片段时才需要某个字形, 此时该字形可以添加到这些片段中任意一个所对应的补丁。 (连字或预组合重音字符可能属于这种情况。)最后,在完成初始片段分析后,同一个 字形可能在 加载两个或多个片段的补丁时都需要。处理这种情况主要有五种策略:
-
可以将两个或多个片段合并到单个补丁中。这样可避免重复 公共字形,但会 增大片段大小。
-
可以将公共字形放入单独的补丁,然后设置映射条目,使加载任何 需要该公共补丁的片段时 同时触发加载该公共补丁。例如,如果 'c' 是片段 'a' 和 'b' 都需要的公共片段,则可以 通过格式 2 映射表设置以下映射条目:
-
子集定义 a → 片段 a
-
子集定义 b → 片段 b
-
子集定义 a 与子集定义 b 的并集 → 片段 c
-
-
在某些情况下,例如使用Unicode 变体选择符时,会有一个 修饰符码位,在与许多其他码位配对时触发字形替换。 由于替代 字形数量庞大,最好将它们保留在独立补丁中,仅当修饰符 码位与相应基础 码位同时存在时才加载。可以通过使用格式 2 补丁映射,并通过childEntryIndices实现多条目匹配。 有关设置方法的示例,请参阅示例 2:带子条目的以字形为键的补丁。
-
或者,可以将该字形包含在两个或多个对应片段的补丁中, 代价是将字形数据复制到 多个补丁。
-
最后,可以将公共字形移入初始字体。这样可避免增加片段大小和 重复字形数据, 但会增大初始字体大小。它还意味着无论是否需要,字形数据都会始终 加载。这 对许多片段都需要或以其他方式使分段复杂化的字形很有用。
在初始字体中预加载数据
在某些情况下,可能希望避免对初始文件应用补丁的开销。例如, 可以 希望在公司主页上加载的字体已经能够渲染该页面上的内容。 这类文件的主要优点 是降低渲染延迟:内容可以在一次往返后渲染。
在下载的字体文件中包含数据有两种方法。一种是直接将增量字体整体编码, 使数据位于初始文件中。任何此类数据都将始终可用于字体的任何补丁版本。 当相同数据原本需要出现在多个不同片段中时,这种方法可能有用。
另一种方法是下载已应用补丁的字体。也就是说,可以编码一个“基础” 文件,其中只有少量或没有数据, 但随后在服务器端将补丁应用到该文件,使下载的文件已经包含 这些补丁中的数据。
只需要一个预加载版本的字体时,这些策略的结果大致相同, 但第一种方法既 更简单,在某些情况下也更具体。但是,需要多个预加载字体时, 预修补方法通常 更好。使用第一种方法时,需要生成多个编码,每个预加载文件一个。使用 第二种方法时,所有预加载文件仍共享相同的整体补丁图,这既 减少存储补丁所需的总空间, 又提高 CDN 缓存效率,因为所有预加载文件都会从同一 集合中选择后续补丁。
表排序
在初始字体文件中(无论是否编码为 woff2),可以自定义 文件内实际表字节的顺序。 编码器应考虑尽可能早地放置映射表(IFT 和 IFTX)以及解码 映射表所需的任何其他表(cmap)。这样,经过优化的客户端实现可以在收到全部字体数据之前 访问 补丁映射,并可能更早发起任何所需 补丁的请求。
类似地,以表为键的补丁为每个被修补的表提供独立的 brotli 流,且该格式允许 将这些流按任意顺序放入 补丁文件。因此,出于相同原因,编码器应考虑尽可能早地在补丁文件中放置 映射表更新,以及解码映射表所需的任何 其他表。
选择输入 ID 编码
本规范支持两种将补丁 ID 嵌入 URL 模板的编码。第一种是base32hex, 它很适合通常存储在文件系统中的预生成补丁。Base32hex 编码仅使用 字符 0-9 和 A-V,这些字符在所有常用文件系统中都可安全用于文件名,并且不会因 大小写不敏感而发生冲突。由于字符串嵌入时不带填充,此格式无法可靠解码, 因此对于动态生成的补丁可能不是理想 选择。另一种编码是base64url,它是适合嵌入 URL 或区分大小写文件系统的 base64 编码变体。使用此编码时,id 会带填充嵌入,从而 可以可靠解码该值。
单个字符选择符 d1 到 d4 仅相对于 base32hex 编码的 id。这些选择符 通常用于 通过根据 id 编码末尾字符将相关文件分散到一个或多个 子目录层级中,减少单个文件系统目录中存储的文件数量。使用整数 id 时,这些字符通常会在各数字之间 均匀分布,但对于字符串 id,可能分布不均甚至恒定。希望将字符串 id 与 d1 到 d4 一起使用的编码器,应注意使 id 字符串的末尾发生变化。将 d1 到 d4 与使用 base64url 编码的 id 混合使用是有效的。
8. 隐私 注意事项
8.1. 根据字符集推断内容
IFT 会向承载 Web 字体的服务器暴露有关浏览器希望使用该字体 渲染的字符集合的信息(有关 详细信息,请参阅 § 4 扩展字体子集)。
对于使用非常庞大字符集的某些语言(例如中文和日文),传输 总字节数的大幅减少 意味着 Web 字体首次变得可用,包括在移动网络上。 但是,对于这些语言,恶意字体服务器可能会分析单个请求,以获取 有关正在阅读的内容 类型的信息。除非请求的 字符非常罕见,否则尚不清楚这种攻击的可行性,或利用它所需的计算复杂度。
更具体地说,IFT 字体包含一组 Unicode 码位分组,并会为与 正在渲染内容相交的分组发出请求。这向承载服务器提供信息,表明某个 分组中至少需要一个码位,但不 包含有关该分组中具体需要哪些码位的信息。从功能上看,这与现有的CSS 字体 4 § 4.5 字符范围:unicode-range 描述符非常相似,并具有相同的隐私 影响。有关 unicode-range 隐私影响的讨论可 在 CSS 字体 4 规范中找到:
-
CSS 字体 4 § 15.1 此特性 可能向 Web 站点或其他方暴露哪些信息?这种暴露对于哪些目的是 必要的?。对于隐私尤其敏感的上下文,该节建议用户代理 下载文档中的所有 Web 字体。 对于 IFT 字体,使用§ 4.7 完全扩展字体将 获取整个可用 IFT 字体,而不会提供有关 具体内容的信息。或者,在隐私敏感上下文中,用户代理可以 随机选择当前内容不需要的其他补丁, 以混淆实际需要哪些补丁。
-
CSS 字体 4 § 15.8 本规范向 源暴露哪些数据?还请说明哪些数据与其他特性在相同或不同上下文中 暴露的数据相同。
当内容作者和/或站点作者选择使用 IFT 字体时,他们必然会对 编码该 字体的人以及加载该字体的服务给予一定信任,因为如上所述,在扩展 IFT 字体时, 会传输一些有关正在渲染内容的信息。(二者之间存在平衡, 因为如下所述, 编码时越谨慎,对服务的信任要求就越低。) 因此, 对于隐私敏感场景,建议自行托管 IFT 字体,因为这样可防止将有关 内容的任何信息传输给第三方。此外,在这种情况下,建议配置内容 安全策略设置,禁止从不同源加载字体。
8.2. IFT 字体编码与隐私
IFT 字体的编码方式,会显著影响从补丁文件传输中 推断出多少有关内容的信息。本节提供一些通用指导,用于 评估编码 IFT 字体时所作选择的隐私影响。
IFT 字体会拆分为一组带有关联激活条件的补丁。当 发出补丁请求时, 它传达出内容包含某些与该补丁激活条件相交的信息。因此, 字体中激活条件的结构,是评估编码隐私 特性时最重要的方面。
字体支持的每个唯一码位、特性和设计空间配置都会提供一个潜在 信号: 它是否存在。激活条件可以是析取条件或合取条件。析取 条件会引入不确定性,因为激活只表示 条件中的至少一个单独项目存在。另一方面,合取补丁不会增加不确定性, 因为它们要求每个单独项目同时 存在。
码位或特性的典型出现频率会影响其存在所传达的信息量。 高频出现的项目传达的信息少于低 频项目。低频码位的存在会大幅缩小可能内容的集合。 从高层次来看,这意味着对于包含低频 项目的条件,应使用更大、粒度更低的条件。
本规范制定期间进行的模拟 发现,(对于析取 条件)最小分组大小为 4 到 7 个项目可产生良好的模糊性。在编码中使用最小分组大小 从性能角度看也 很合理,因为粒度过细的补丁会引入过多开销,导致性能不佳。 由于低频项目很少需要,对包含这些项目的补丁使用最小分组大小 对整体性能影响很小。整体性能通常由高频项目驱动。
编码器应评估生成编码中的每个条件,并确定其有效分组大小。有效分组 大小是跨单独项目(码位、布局标签、设计 空间)中,对触发该条件有贡献的最小析取子条件。对于用于隐私敏感场景的 编码, 编码器应确保所有条件的最小分组大小至少为 4 到 7。例如,考虑以下 情况:
-
A or B or C or D:有效分组大小为 4,满足最低要求。 -
A and B and C AND D:有效分组大小为 1,不满足最低 要求。 -
(A or B or C or D) AND (E or F):由于 (D or E) 子条件,有效分组大小为 2。它不 满足最低要求。
编码器应提供配置控制,用于设置所生成编码的隐私级别,因为 不同使用场景 需要不同级别。例如,通过 https 自行托管的字体编码不会引入 隐私 问题,而计划托管在常见第三方字体服务上的 IFT 字体则不同。在为 隐私敏感度较低的场景编码时,编码器可以选择进行违反建议最小分组 大小的优化。 例如:设置一个条件,用于添加某个可选布局特性的数据,这会产生有效分组 大小 1。
当编码器在给定编码保护隐私的程度方面提供这种灵活性时, 建议 其文档和/或用户界面传达本节中的足够信息,使 生成编码的用户能够作出适当选择。类似地,建议托管 IFT 字体的站点 在编码自己的字体或从第三方获取已编码字体时考虑隐私因素。
8.3. 按源限制可避免指纹识别
根据[css-fonts-4]的要求:
“Web 字体不得 在除与 @font-face 规则关联或拥有 FontFaceSet 的文档之外的任何其他文档中访问。 设备上的其他应用不得 访问 Web 字体。”——CSS 字体 4 § 10.2 Web 字体
由于 IFT 字体在 CSS 中与普通字体采用相同方式处理(§ 2 选择启用机制), 这些要求同样适用,并可避免信息跨 源泄露。
类似要求也适用于字体调色板值:
“作者定义的 字体颜色调色板只能供引用它的文档使用。在引用它的文档之外使用 作者定义的颜色调色板, 会构成安全泄漏,因为一个页面的内容将能够 影响其他页面,攻击者可将其用作攻击向量。”——CSS 字体 4 § 9.2 用户定义的字体颜色调色板:@font-palette-values 规则
9. 安全 注意事项
一个安全问题是,IFT 字体可能生成大量补丁网络请求。 这可能给 客户端或承载补丁的服务带来问题。IFT 规范包含若干 缓解措施,用于限制过多 请求:
-
§ 4 扩展字体子集:禁止多次重新请求同一 URL,并限制扩展过程中 可发出的请求总数。
-
加载补丁文件:规定在实现 Web 浏览器时使用[FETCH], 并匹配初始 字体加载的 CORS 设置。因此,除非承载服务通过适当的 访问控制标头选择启用,否则不允许对补丁文件发出跨源请求。
10. 更改
自2025 年 7 月 31 日的候选 推荐标准快照以来(参见提交历史):
- 标记了可测试的规范性算法
- 区分了客户端与编码器的一致性声明,并添加标记和样式
- 为可测试断言添加了样式
- 从非规范性注释和介绍性定义中移除了无意出现的一致性用语(should)
- 标记了所有可测试的(must)断言
- 将附录 B(示例)标记为非规范性
自2025 年 7 月 15 日的工作 草案以来(参见提交历史):
- 从参考文献中移除了一个未使用的项目
- 定义并导出了术语“塑形单元”
自2025 年 2 月 20 日的工作 草案以来(参见提交 历史):
- 扩展了隐私一节,为编码器提供指导。
- 添加了完整绝对 URL 和主机名相对 URL 扩展的示例。
- 阐明了如何从 'url template bytes' 读取字节。
- 添加了其他错误条件:操作码 0(插入 0 个字面值)被设为无效。如果 插入字面值操作所需的剩余字节不足,则失败。添加了会导致失败的示例。
- 更新 URL 扩展示例,对字节数组使用 C 语法。添加了更多样化的示例, 包括相对 URL 和带查询参数的 URL。
- 根据 TAG 审查,弃用 rfc6570 URI 模板,改为使用操作码编码的模板。
- 添加了 fetch 调用,并使用 processResponseConsumeBody 回调。
- 添加了有关配置 fetch 请求的更具体信息。
- 根据审查反馈重新措辞了条目选择,使其更清晰。
- 为不失效补丁指定了选择/获取顺序。
- 指定了 URL 解析失败时的处理方式。
- 添加了有关存在多个补丁时优化以字形为键补丁应用的注释。
- 在格式 2 解释中,缺少对未指定条目增量/长度的处理。添加了一个 额外步骤,为该情况生成 URL。
- 阐明了与 WOFF2 有关的措辞
- 在字体压缩一节中添加了建议使用 WOFF2 的注释。
- 更新到(当前的)共享 brotli 15 规范
- 添加标志位,以指示可选 charstring 偏移是否存在。
- 添加了有关补丁添加 CFF 表和 charstrings 偏移的注释。
- 对于包含 CFF 或 CFF2 表的字体,在补丁映射中添加了指向 CharStrings 的偏移字段。
- 禁止在 URI 模板扩展中使用未定义的变量名。
- 重新措辞了有关不支持表达式级别时出错的声明。
- 将 URL 模板限制为仅使用一级替换。
- 从 rfc3986 URI 切换到 whatwg/url
- 定义并使用了术语“预取列表”。
- 在失效补丁选择中,优先选择已预加载的条目,从而减少往返次数。
- 更新字体扩展算法,以使用 URL 预加载列表。
- 更新格式 2 解释和条目移除算法,以处理每个条目的多个 URL。
- 重新设计格式 2 编码,使其可以选择支持每个条目的多个 URL。
- 添加了有关以字形为键补丁应用中偏移大小的指导。
- 添加了针对 CFF 和 CFF2 增量字体要求的专门章节。
- 将特性注册表附录更新到当前版本
自2024 年 7 月 9 日的工作 草案以来(参见提交 历史):
- 向附录 B 添加了一个使用子条目的扩展示例
- 在格式 2 解释中,将先前条目与用于输出的过滤列表分开保留。
- 将映射条目中引用的条目称为子条目。
- 根据所引用条目是否相交,重新设计了复制索引机制
- 根据实现工作修改了格式 1 和格式 2 补丁映射编码(例如 澄清、错误处理和 修复疏漏)。
- 添加了有关缓存增量字体的章节。
- 添加了有关默认布局特性的指导。
- 移除了纯 brotli 补丁格式,剩余补丁格式现为以表为键和以字形为键。
- 添加了检查 IFT 字体能够渲染哪些内容的过程。
- 向扩展算法添加了显式预取。
- 添加了附录 B,其中包含扩展算法执行示例。
附录 A:默认特性 标签
本附录为非规范性内容。它提供一组布局特性, 这些特性被认为 在大多数塑形器实现中默认使用。此列表汇总自:
-
在[enabling-typography] 中列为“默认开启”的特性
-
harfbuzz 子集化工具默认集合中的特性。
塑形器默认使用的布局特性
| 标签 | 名称 |
|---|---|
| abvf | 基线上方形式 |
| abvm | 基线上方标记定位 |
| abvs | 基线上方替换 |
| akhn | Akhand |
| blwf | 基线下方形式 |
| blwm | 基线下方标记定位 |
| blws | 基线下方替换 |
| calt | 上下文替代形式 |
| ccmp | 字形组合/分解 |
| cfar | Ro 之后的合取形式 |
| chws | 上下文半宽间距 |
| cjct | 合取形式 |
| clig | 上下文连字 |
| cswh | 上下文花体 |
| curs | 草书定位 |
| dist | 距离 |
| dnom | 分母 |
| dtls | 无点形式 |
| fin2 | 终止形式 #2 |
| fin3 | 终止形式 #3 |
| fina | 终止形式 |
| flac | 扁平重音形式 |
| frac | 分数 |
| half | 半形式 |
| haln | Halant 形式 |
| init | 初始形式 |
| isol | 独立形式 |
| jalt | 对齐替代形式 |
| kern | 字距调整 |
| liga | 标准连字 |
| ljmo | 前导 Jamo 形式 |
| locl | 本地化形式 |
| ltra | 从左到右替代形式 |
| ltrm | 从左到右镜像形式 |
| mark | 标记定位 |
| med2 | 中间形式 #2 |
| medi | 中间形式 |
| mkmk | 标记到标记定位 |
| mset | 通过替换进行标记定位 |
| nukt | Nukta 形式 |
| numr | 分子 |
| pref | 基线前形式 |
| pres | 基线前替换 |
| pstf | 基线后形式 |
| psts | 基线后替换 |
| rand | 随机化 |
| rclt | 必需的上下文替代形式 |
| rkrf | Rakar 形式 |
| rlig | 必需连字 |
| rphf | Reph 形式 |
| rtla | 从右到左替代形式 |
| rtlm | 从右到左镜像形式 |
| rvrn | 必需的变体替代形式 |
| ssty | 数学脚本样式替代形式 |
| stch | 拉伸字形分解 |
| tjmo | 尾随 Jamo 形式 |
| valt | 替代垂直度量 |
| vatu | Vattu 变体 |
| vchw | 垂直上下文半宽间距 |
| vert | 垂直书写 |
| vjmo | 元音 Jamo 形式 |
| vkrn | 垂直字距调整 |
| vpal | 成比例的替代垂直度量 |
| vrt2 | 垂直替代形式与旋转 |
| vrtr | 用于旋转的垂直替代形式 |
附录 B:扩展算法 执行示例
本附录为非规范性内容。它提供典型 IFT 字体如何由 § 4.3 增量字体扩展算法处理的示例。
示例 1:以表和字形为键的 补丁
在此示例中,IFT 字体混合包含§ 6.2 以表为键和§ 6.3 以字形为键补丁。
初始字体:包含以下 IFT 和 IFTX 补丁映射。注:当子集定义中未指定 特性和设计空间 集合时,它们默认为空集合。
| 表 = "IFT " | 兼容性 ID = 0x0000_0000_0000_0001 | |
|---|---|---|
| 子集定义 | URL | 格式编号 |
code points: { 'a', 'b', ..., 'z' }
| //foo.bar/01.tk | 2,以表为键——部分失效 |
code points: { 'A', 'B', ..., 'Z' }
| //foo.bar/02.tk | 2,以表为键——部分失效 |
code points: { '0', '1', ..., '9' }
| //foo.bar/03.tk | 2,以表为键——部分失效 |
code points: { 'a', 'b', ..., 'z',
'A', 'B', ..., 'Z' }
| //foo.bar/04.tk | 2,以表为键——部分失效 |
code points: { 'a', 'b', ..., 'z',
'A', 'B', ..., 'Z',
'0', '1', ..., '9' }
| //foo.bar/05.tk | 2,以表为键——部分失效 |
| 表 = "IFTX" | 兼容性 ID = 0x0000_0000_0000_0002 | |
|---|---|---|
| 子集定义 | URL | 格式编号 |
code points: { 'a', 'b', ..., 'm' }
| //foo.bar/01.gk | 3,以字形为键 |
code points: { 'n', 'o', ..., 'z' }
| //foo.bar/02.gk | 3,以字形为键 |
code points: { 'A', 'B', ..., 'M' }
| //foo.bar/03.gk | 3,以字形为键 |
code points: { 'N', 'O', ..., 'Z' }
| //foo.bar/04.gk | 3,以字形为键 |
code points: { '0', '1', ..., '9' }
| //foo.bar/05.gk | 3,以字形为键 |
优化扩展示例
注:此示例执行按照 经过优化的客户端实现来描述,该实现旨在减少为目标子集定义扩展字体所需的 往返次数。此优化执行完全符合 扩展算法,但会比扩展算法规定的时间更早 加载某些 URL(以字形为键的 URL)。这是允许的,因为这些补丁 不会被之前以表为键的补丁使之失效。
输入:
-
包含上述 IFT 和 IFTX 表的初始字体。
-
目标子集定义:code points = {'f', 'P'}
迭代 1:
-
步骤 1 到 6——对初始字体中的所有 条目应用检查条目交集,以下补丁条目相交:
-
//foo.bar/01.tk
-
//foo.bar/02.tk
-
//foo.bar/04.tk
-
//foo.bar/05.tk
-
//foo.bar/01.gk
-
//foo.bar/04.gk
-
-
步骤 8——必须从该集合中选择一个条目。没有完全失效条目,因此必须 选择一个部分 失效条目(//foo.bar/*.tk)。按照§ 4.4 选择失效补丁, 候选条目与目标子集定义有以下交集:
-
//foo.bar/01.tk - {'f'}
-
//foo.bar/02.tk - {'P'}
-
//foo.bar/04.tk - {'f', 'P'}
-
//foo.bar/05.tk - {'f', 'P'}
//foo.bar/01.tk 和 //foo.bar/02.tk 的交集都是 //foo.bar/04.tk 和 //foo.bar/05.tk 的真子集,因此不满足选择 条件。剩下 //foo.bar/04.tk 或 //foo.bar/05.tk。由于 //foo.bar/04.tk 在补丁映射中 先列出,因此选择它。
-
-
步骤 9——获取所选补丁 //foo.bar/04.tk。此外,作为优化,同时开始获取 两个以字形为键的 补丁 //foo.bar/01.gk 和 //foo.bar/04.gk。以表为键的补丁 //foo.bar/04.tk 为部分 失效,这意味着应用 //foo.bar/04.tk 后,这两个以字形为键的补丁 仍然有效。因此,客户端可以预期 后续迭代会需要这两个以字形为键的补丁,并在此时开始加载它们。
-
步骤 10——将获取的补丁 //foo.bar/04.tk 应用于初始字体。此补丁更新除 glyf 和 loca 之外的所有表, 以添加对码位 'a' 到 'Z' 的支持。此外,“IFT ”表中的映射 更新为:
表 = "IFT " 兼容性 ID = 0x0000_0000_0000_0006 子集定义 URL 格式编号 code points: { '0', '1', ..., '9' }//foo.bar/08.tk 2,以表为键——部分失效 请注意以下更改:
-
包含 'a'、...、'z' 和 'A'、...、'Z' 数据的所有条目都已移除。 只剩一个用于剩余 '0'、...、'9' 码位的条目。
-
字体二进制文件已更改,因此之前列出的以表为键补丁不再 有效。因此,兼容性 ID 已更改, 剩余 '0'、...、'9' 条目的 URL 也已更改。新 URL //foo.bar/08.tk 处的补丁与更新后的字体兼容。
-
迭代 2:
-
步骤 2 到 6——对上一次迭代中更新后的字体的所有条目应用检查条目交集,以下补丁 条目相交:
-
//foo.bar/01.gk
-
//foo.bar/04.gk
-
-
步骤 8——必须从该集合中选择一个条目。只剩下不失效补丁,客户端可以 自由选择任意一个。在此情况下,它选择 第一个 //foo.bar/01.gk。
-
步骤 9——//foo.bar/01.gk 的获取已在迭代 1 中启动。
-
步骤 10——将获取的补丁 //foo.bar/01.gk 应用于当前字体子集。该补丁向 glyf 和 loca 表 添加码位 'a' 到 'm' 的 字形数据。此外,“IFTX”表中的映射被更新,以移除 //foo.bar/01.gk 的条目。其他条目的兼容性 ID 和 URL 保持不变。
迭代 3:
-
步骤 2 到 6——对上一次迭代中更新后的字体的所有条目应用检查条目交集,以下补丁 条目相交:
-
//foo.bar/04.gk
-
-
步骤 8——只剩一个条目,选择它。
-
步骤 9——//foo.bar/04.gk 的获取已在迭代 1 中启动。
-
步骤 10——将获取的补丁 //foo.bar/04.gk 应用于当前字体子集。该补丁向 glyf 和 loca 表 添加码位 'N' 到 'Z' 的 字形数据。此外,“IFTX”表中的映射被更新,以移除 //foo.bar/04.gk 的条目。其他条目的兼容性 ID 和 URL 保持不变。
迭代 4:
-
步骤 2 到 6——不再有相交条目,因此算法终止。返回字体 子集,此时它已可用于渲染 目标子集定义覆盖的任何内容。
示例 2:带 子条目的以字形为键的补丁
在此示例中,IFT 字体包含一组§ 6.3 以字形为键补丁, 它们利用子条目正确处理 UVS 替换补丁的包含。对于每组基础字形,都有一组 通过变体选择符进行替换的替代字形。 这些字形保留在单独补丁中。替代字形 补丁通过子条目配置, 仅当基础字形和变体选择符同时存在时才加载。
初始字体:包含以下 IFT 补丁映射。注:当 子集定义中未指定码位、特性、设计空间 或子条目集合时,它们默认为空集合。
| 表 = "IFT " | 兼容性 ID = 0x0000_0000_0000_0001 | |||
|---|---|---|---|---|
| 子集定义 | URL | 格式编号 | 忽略? | 注释 |
code points:
{ 'a', 'b', ..., 'm' }
| //foo.bar/01.gk | 3,以字形为键 | 否 | |
code points:
{ 'n', 'o', ..., 'z' }
| //foo.bar/02.gk | 3,以字形为键 | 否 | |
code points: { VS1 }
| N/A | 3,以字形为键 | 是 | |
child entries: [0, 2], match mode: conjunctive | //foo.bar/03.gk | 3,以字形为键 | 否 | 包含由 VS1 激活的 {'a', ..., 'm'} 替代字形 |
child entries: [1, 2], match mode: conjunctive | //foo.bar/04.gk | 3,以字形为键 | 否 | 包含由 VS1 激活的 {'n', ..., 'z'} 替代字形 |
扩展示例 1
输入:
-
包含上述 IFT 表的初始字体。
-
目标子集定义:code points = {'f'}
迭代 1:
-
步骤 1 到 6——对初始字体中的所有 条目应用检查条目交集,以下补丁条目相交:
-
//foo.bar/01.gk
-
-
步骤 8 到 10——只有一个补丁 //foo.bar/01.gk,选择、加载并应用它。
迭代 2:
-
步骤 2 到 6——不再有相交条目,因此算法终止。返回字体 子集,此时它已可用于渲染 目标子集定义覆盖的任何内容。
扩展示例 2
输入:
-
包含上述 IFT 表的初始字体。
-
目标子集定义:code points = {'f', VS1}
迭代 1:
-
步骤 1 到 6——对初始字体中的所有 条目应用检查条目交集,以下补丁条目相交:
-
//foo.bar/01.gk
-
//foo.bar/03.gk
-
-
步骤 8——必须选择一个条目,在此情况下客户端实现可以自由选择任意一个。 选择 //foo.bar/01.gk。
-
步骤 9——由于两个相交条目都是不失效条目,因此可以同时加载。
-
步骤 10——将 //foo.bar/01.gk 应用于字体。映射表被更新,将 //foo.bar/01.gk 的条目标记为忽略。
迭代 2:
-
步骤 1 到 6——对当前字体中的所有 条目应用检查条目交集,以下补丁条目相交:
-
//foo.bar/03.gk
-
-
步骤 8 到 10——只有一个补丁 //foo.bar/03.gk,选择并应用它(它已在 上一次迭代期间加载)。
迭代 3:
-
步骤 2 到 6——不再有相交条目,因此算法终止。返回字体 子集,此时它已可用于渲染 目标子集定义覆盖的任何内容。