本地字体访问 API

社区组草案报告,

本版本:
https://wicg.github.io/local-font-access/
测试套件:
https://github.com/web-platform-tests/wpt/tree/master/font-access
问题追踪:
GitHub
规范内
编辑:
(Google公司)
前编辑:
Emil A. Eklund
Alex Russell
Olivier Yiptong

摘要

本规范记录了网页浏览器支持允许用户授权网站枚举系统中所有可用字体、并访问字体原始数据的功能,从而实现对文本更细致的自定义渲染。

本文档状态

本规范由 Web 平台孵化器社区组 发布。 这不是 W3C 标准,也未被纳入 W3C 标准路线。 请注意,根据 W3C 社区贡献者许可协议(CLA),有有限的选择退出权及其他相关条件适用。 了解更多关于 W3C 社区与商业小组 信息。

1. 简介

本规范描述了一套用于网页浏览器的字体枚举 API,浏览器可选择性地允许用户授予对全部可用系统字体集的访问权限。对于每种字体,可以对 SFNT [SFNT] 容器或等效项进行底层(字节级)访问,从而获取完整的字体数据。

Web 开发者过去只能通过启发式信息了解哪些本地字体可用于页面内容样式,通常会在 CSS 中包含复杂的 font-family 列表,用以启发式地控制字体回退。为设计师生成合适的回退方案极为复杂,因而出现了帮助“目测”可能可用本地字体匹配项的工具。

字体枚举能够帮助:

尽管 Web 起源于以文本为主,并且用户代理对高质量排版的支持非常完善,但在某些类型的 Web 应用场景下仍然有限制:

高品质的设计和图形工具在 Web 平台上一直较难实现,这些工具通常将丰富的排版特性和控制作为核心能力。

本 API 使这些工具能够访问与浏览器布局及光栅化引擎用于绘制文本时一致的底层字体数据。例如用于字形矢量数据的 OpenType glyf 表、用于字形定位的 GPOS 表,以及用于连字和其他字形替换的 GSUB 表。这些信息对于上述工具来说,既能保证输出结果的跨平台性(通过嵌入矢量描述而非码位),也能支持以字体为基础的艺术创作(将字体作为可操作形状的基础)。

2. 目标

该 API 应当:

虽然上文目标中提到 Worker 支持,但目前该 API 按规范只暴露于 Window 环境。

3. 示例

本节为非规范性内容。

3.1. 枚举本地字体

该 API 允许脚本枚举本地字体,包括每种字体的相关属性。

以下代码查询可用本地字体,并将每个字体的名称及指标输出到控制台。
showLocalFontsButton.onclick = async function() {
  try {
    const array = await self.queryLocalFonts();

    array.forEach(font => {
      console.log(font.postscriptName);
      console.log(` full name: ${font.fullName}`);
      console.log(` family: ${font.family}`);
      console.log(` style: ${font.style}`);
    });
   } catch(e) {
    // 处理错误,例如用户取消了操作。
    console.warn(`Local font access not available: ${e.message}`);
  }
};

3.2. 使用本地字体样式化

高级创作工具可以让用户用所有可用本地字体对文本进行样式化。在此场景下,获取本地字体名称能够提供更丰富的选择:

以下代码将可用本地字体填充到下拉选择框,可用于编辑类应用的用户界面。

useLocalFontsButton.onclick = async function() {
  try {
    // 查询允许访问的本地字体。
    const array = await self.queryLocalFonts();

    // 创建用于样式化的元素。
    const exampleText = document.createElement("p");
    exampleText.id = "exampleText";
    exampleText.innerText = "The quick brown fox jumps over the lazy dog";
    exampleText.style.fontFamily = "dynamic-font";

    // 创建字体选择列表及响应处理。
    const textStyle = document.createElement("style");
    const fontSelect = document.createElement("select");
    fontSelect.onchange = e => {
      const postscriptName = fontSelect.value;
      console.log("selected:", postscriptName);
      // 使用 @font-face src: local 匹配进行样式化的示例。
      textStyle.textContent = `
        @font-face {
          font-family: "dynamic-font";
          src: local("${postscriptName}");
        }`;
    };

    // 用所有可用字体填充选择列表。
    array.forEach(font => {
      const option = document.createElement("option");
      option.text = font.fullName;
      // postscriptName 可用于 @font-face src: local 对元素样式化。
      option.value = font.postscriptName;
      fontSelect.append(option);
    });

    // 将所有元素添加至页面。
    document.body.appendChild(textStyle);
    document.body.appendChild(exampleText);
    document.body.appendChild(fontSelect);
  } catch(e) {
    // 处理错误,例如用户取消了操作。
    console.warn(`Local font access not available: ${e.message}`);
  }
};

3.3. 访问字体数据

该 API 允许脚本请求字体数据。

以下代码查询可用本地字体,并输出每个字体的详细信息到控制台。

这里用枚举来获取本地字体数据,可以解析具体的字体表,也可输入到如 HarfBuzz 或 Freetype 的 WASM 版本:

useLocalFontsButton.onclick = async function() {
  try {
    const array = await self.queryLocalFonts();

    array.forEach(font => {
      // blob() 返回包含字体字节的 Blob。
      const bytes = await font.blob();

      // 检查前四个字节,SFNT 用于定义格式。
      // 规范: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
      const sfntVersion = await bytes.slice(0, 4).text();

      let outlineFormat = "UNKNOWN";
      switch (sfntVersion) {
        case '\x00\x01\x00\x00':
        case 'true':
        case 'typ1':
          outlineFormat = "truetype";
          break;
        case 'OTTO':
          outlineFormat = "cff";
          break;
      }
      console.log(`${font.fullName} outline format: ${outlineFormat}`);
    }
  } catch(e) {
    // 处理错误。可能是权限错误。
    console.warn(`Local font access not available: ${e.message}`);
  }
};

更详细解析字体文件,比如枚举包含的全部表现,超出本规范范围。

3.4. 请求特定字体

在某些场景下,Web 应用可能希望请求访问特定字体。例如呈现之前创作内容时需要嵌入字体名称。queryLocalFonts() 方法可接收 postscriptNames 参数,仅返回那些 PostScript 名称完全匹配的字体。

用户代理可能会采用不同的 UI 支持这些操作。例如,如果指纹风险很低,该请求可无需用户授权直接满足。也可能弹出字体选择器,仅显示请求的字体。

// 需要用户激活。
requestFontsButton.onclick = async function() {
  try {
    const array = await self.queryLocalFonts({postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic']});

    array.forEach(font => {
      console.log(`Access granted for ${font.postscriptName}`);
    });

  } catch(e) { 
    // 处理错误。可能是权限错误。
    console.warn(`Local font access not available: ${e.message}`);
  }
};

4. 概念

用户语言是一个有效的 BCP 47 语言标签,代表可能的语言或用户最偏好的语言。[BCP47]

4.1. 字体表示

字体表示是字体的某种具体表现。例如 OpenType、TrueType、位图字体、Type1 字体、SVG 字体以及未来的字体格式。本规范定义字体表示的属性包括:

注意:一个字体表示的数据字节一般应为用户文件系统上的字体文件逐字节一致的内容。用户代理不需要规范化字体数据,因此同一用户在某一操作系统上的字体表示不会因用户代理不同而变化。不规范化有助于实现 Web 应用在内容创作时文本渲染的完整字体保真。

注意:本规范并未严格定义以上字段在任何特定字体格式下的值,以防操作系统差异导致用户代理不合规。但对 OpenType [OPENTYPE]字体,推荐值如下:

这些名称属性参照 [CSS-FONTS-4] 中的属性用法,例如 @font-face、font-family 等。PostScript 名称可作为唯一键(例如指定或匹配内容时),全称和家族名称用于用户可见的字体选择 UI,样式名称可用于更具体选择。

有效的 PostScript 名称是标量值字符串,长度小于 64,只包括 U+0021(!)到 U+007E(~)范围的字符,但排除以下 10 个码位: U+005B ([), U+005D (]), U+0028(左括号), U+0029(右括号), U+007B({), U+007D(}), U+003C(<), U+003E(>), U+002F(/), U+0025(%)。

注意:此要求旨在符合 nameID = 6 在 [OPENTYPE] 中的要求。

4.2. 系统字体

系统字体是由操作系统提供并在系统范围可用的字体。

将系统字体作为字体表示读取,即生成与字体等价的字体表示,需提供全部字体表示属性:

如果无法提供字体表示,则该操作可能失败。

用户代理可使用任何算法为系统字体生成字体表示。实践中,现代操作系统和系统 API 支持以 SFNT [SFNT] 字体文件格式存储的字体,如 OpenType、TrueType、Web Open Font Format 等,满足这些要求,也可以高效枚举字体集合,并同时提供每个字体的常用名称属性。

要获取所有系统字体表示,请按以下步骤操作:
  1. 令 fonts 为所有系统字体的列表。

  2. 令 result 为新的列表。

  3. 对于 fonts 中的每个 font:

    1. 令 representation 为 font作为字体表示读取。失败时跳过。

    2. 如果用户代理判断该字体绝不能暴露给网站,则可跳过。

    3. 将 representation 添加到 result。

  4. 返回 result。

5. 权限集成

枚举本地字体需要获得授权。

5.1. 权限

本地字体访问 API 是一个默认高级功能,由名称 name "local-fonts" 标识。

调用 queryLocalFonts() API 时,用户代理可以呈现字体选择列表、是/否选项,或其他界面方式。用户代理应以合适方式呈现选择结果到权限上。例如,若用户选择了要暴露给站点的字体集且后续 API 调用会返回该集,则权限状态可为 "granted";若会再次提示用户,则权限状态可为 "prompt"。

可使用 navigator.permissions API 查询枚举本地字体的权限:
// 此操作仅查询现有权限状态,不会更改它。
const status = await navigator.permissions.query({ name: "local-fonts" });
if (status.state === "granted")
  console.log("permission was granted 👍");
else if (status.state === "prompt")
  console.log("permission will be requested");
else
  console.log("permission was denied 👎");

5.2. 权限策略

本规范定义了用字符串 "local-fonts" 标识的策略受控功能。其 默认许可列表为 'self'。

注意: 默认许可列表 'self' 允许此功能默认在同源嵌套框架中使用,但禁止第三方内容访问。

可以通过在 iframe 元素添加 allow="local-fonts" 属性,选择性允许第三方使用:

<iframe src="https://example.com" allow="local-fonts"></iframe>

也可以通过 HTTP 响应头声明权限策略,完全禁止第一方环境使用:

Permissions-Policy: local-fonts 'none'

详情见 [PERMISSIONS-POLICY]。

6. API

6.1. 字体任务源

字体任务源是一个新的通用任务源,用于本规范中排队的所有任务。

6.2. 字体管理器

await self . queryLocalFonts()
await self . queryLocalFonts({ postscriptNames: [ ... ] })

异步查询可用/允许的字体。成功时,返回的 Promise 解析为一组 FontData 对象。

如果方法调用时文档未处于 临时激活(如响应点击事件),则返回的 Promise 将被拒绝。

用户将被提示授予本地字体访问权限或选择要提供给站点的字体。如果权限未获得,Promise 也会被拒绝。

如果给定 postscriptNames 选项,则仅返回 PostScript 名称匹配的字体。

[SecureContext]
partial interface Window {
  Promise<sequence<FontData>> queryLocalFonts(optional QueryOptions options = {});
};

dictionary QueryOptions {
  sequence<DOMString> postscriptNames;
};
queryLocalFonts(options) 方法步骤如下:
  1. 令 promise 为新的 Promise。

  2. 令 descriptor 为 PermissionDescriptor,其 name 设置为 "local-fonts"。

  3. 如果 this 的相关设置对象的来源是 不透明来源,则用 "SecurityError" DOMException 拒绝 promise,并返回 promise。

  4. 如果 this 的相关全局对象的关联文档未被允许使用名为 "local-fonts" 的策略受控功能,则用 "SecurityError" DOMException 拒绝 promise,返回。

  5. 如果 this 的相关全局对象无临时激活,则用 "SecurityError" DOMException 拒绝 promise,返回。

  6. 否则,并行运行以下步骤:

    1. 令 system fonts 取 所有系统字体表示。

    2. 令 selectable fonts 为新列表。

    3. 遍历 system fonts 中每个字体 representation,执行:

      1. 令 postscriptName 为 representation 的PostScript 名称。

      2. 断言:postscriptName 是有效的 PostScript 名称。

      3. 如果 options["postscriptNames"] 存在且不包含 postscriptName,则跳过。

      4. 添加绑定 representation 的新 FontData 实例到 selectable fonts。

    4. 提示用户选择 selectable fonts 中一个或多个条目,descriptor 和 allowMultiple 设置为 true,令 result 为结果。 用户代理可仅返回全部字体列表,而不用显示选择列表。

    5. 若 result 为 "denied",则用 "NotAllowedError" DOMException 拒绝 promise,并中止。

    6. 用 postscriptName 升序排序 result,并以排序结果存储 result。

    7. 在字体任务源上队列任务,将 result 解析到 promise。

  7. 返回 promise。

移至 WindowOrWorkerGlobalScope 并理清权限问题。

6.3. FontData 接口

FontData 提供有关字体样式的详细信息。每个 FontData 都有一个对应的 字体表示。

fontdata . postscriptName

字体的 PostScript 名称。例如:“Arial-Bold”。

fontdata . fullName

字体全名,包括家族和子家族。例如:“Arial Bold”

fontdata . family

字体家族名。对应 CSS 的font-family 属性。例如:“Arial”

fontdata . style

字体风格(或子家族)名称。例如:“Regular”、“Bold Italic”

[Exposed=Window]
interface FontData {
  Promise<Blob> blob();

  // 名称
  readonly attribute USVString postscriptName;
  readonly attribute USVString fullName;
  readonly attribute USVString family;
  readonly attribute USVString style;
};
postscriptName getter 步骤如下:
  1. 令 postscriptName 为此对象的PostScript 名称。

  2. 断言:postscriptName 是有效的 PostScript 名称。

  3. 返回 postscriptName。

fullName getter 步骤为返回此对象关联 字体表示的全名。

family getter 步骤为返回此对象关联 字体表示的家族名称。

style getter 步骤为返回此对象关联 字体表示的样式名称。

考虑让 FontData 成为可序列化对象,以便 queryLocalFonts() 的结果可传递给 Workers。

await blob = fontdata . blob()

申请字体的底层字节内容。其结果 blob 包含数据字节。

blob() 方法步骤如下:

  1. 令 realm 为此对象的相关 Realm。

  2. 令 promise 在 realm 中为新的 Promise。

  3. 并行执行以下步骤:

    1. 令 bytes 为此对象关联 字体表示的数据字节。

    2. 令 type 为 `application/octet-stream`。

    3. 在字体任务源上队列任务为:

    4. 令 blob 为 realm 中新建的 Blob,内容为 bytes,type 属性为 type。

    5. 解析 promise,值为 blob。

  4. 返回 promise。

7. 国际化注意事项

记录除字符串本地化之外的国际化注意事项,例如 https://github.com/WICG/local-font-access/issues/72、https://github.com/WICG/local-font-access/issues/59 等。

7.1. 字体名称

在 OpenType 字体中,`name` 表允许名称(家族、子家族等)拥有多语言字符串,可用平台相关的数字语言标识符,或符合 [BCP47] 的语言标签字符串。例如,一个字体可以为 `en-US` 和 `zh-Hant-HK` 同时定义家族名称字符串。

本 API 提供的 FontData 属性 postscriptName、 fullName、 family、 和 style 都是以字符串形式提供的,根据名称会优先使用美国英语本地化或用户语言本地化,否则回退为第一个本地化版本。

Web 应用如需其它语言的名称,可以直接请求和解析 `name` 表。

是否应为 queryLocalFonts() 方法定义参数来指定字符串的目标语言(例如 {lang: 'zh'}),未找到时回退至 `en-US`?或者提供全部名称的访问,例如以 [BCP47] 语言标签为键的 name 映射?[Issue #69]

8. 无障碍注意事项

当前没有发现该功能会影响无障碍访问。

9. 安全性注意事项

当前没有发现该功能会影响安全性。

10. 隐私注意事项

10.1. 指纹识别

字体数据包括:

这些都可以为区别用户提供若干“熵位”。

用户代理可以在特定场景下进行缓解(如权限被拒绝或在隐私浏览/“无痕”模式下),例如只枚举一组随用户代理提供的固定字体集合。

用户代理还可以允许用户选择通过 API 公开给网站的一组字体。

当字体提供多种本地化名称,用户的区域设定信息可能通过字体名称被暴露。用户代理如暴露区域设定信息,应保证其与 navigator.language 所暴露的区域设定一致。

10.2. 身份识别

某个组织的用户可能安装了特定字体。例如,“示例公司”的员工可能系统管理员为他们安装了“Example Corporate Typeface”,可通过网站将其识别为该企业员工。

有些服务可根据手写样本生成字体。如果这些字体被命名为包含个人信息(如 "Alice’s Handwriting Font"),则个人信息可能被暴露。当这些信息包含在字体属性中而不仅是字体名称时,用户可能并不知情。

11. 致谢

我们感谢以下人员的贡献:

再次特别感谢 Tab Atkins, Jr.,他开发并维护了 Bikeshed,即本规范使用的文档编写工具。

同时感谢 Anne van Kesteren、 Chase Phillips、 Domenic Denicola、 Dominik Röttsches、 Igor Kopylov、 Jake Archibald 和 Jeffrey Yasskin 提供了建议、评审和其他反馈。

一致性

文档约定

一致性要求通过描述性断言与 RFC 2119 术语结合表达。 规范性部分中的关键词 “MUST”、 “MUST NOT”、 “REQUIRED”、 “SHALL”、 “SHALL NOT”、 “SHOULD”、 “SHOULD NOT”、 “RECOMMENDED”、 “MAY” 和 “OPTIONAL” 应按照 RFC 2119 的定义解释。 但为便于阅读, 本规范未全部采用大写字母。

除明确标注为非规范性内容、示例和说明外,本规范全部内容均为规范性内容。[RFC2119]

规范中的示例会以 “例如” 开头, 或和规范性文本区隔,使用 class="example",如下:

这是一条有帮助的示例说明。

说明性备注以 “注意” 开头, 并用 class="note" 区分,例如:

注意,这是一条说明性备注。

索引

本规范定义的术语

引用规范中定义的术语

参考文献

规范性引用

[BCP47]
A. Phillips, Ed.; M. Davis, Ed.. Tags for Identifying Languages. September 2009. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc5646
[FileAPI]
Marijn Kruisselbrink. File API. URL: https://w3c.github.io/FileAPI/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[OPENTYPE]
OpenType specification. URL: http://www.microsoft.com/typography/otspec/default.htm
[PERMISSIONS]
Marcos Caceres; Mike Taylor. Permissions. URL: https://w3c.github.io/permissions/
[PERMISSIONS-POLICY]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[SFNT]
Spline/Scalable font format. January 2019. URL: https://www.iso.org/obp/ui/#iso:std:iso-iec:14496:-22:ed-4:v1:en
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

说明性引用

[CSS-FONTS-4]
Chris Lilley. CSS Fonts Module Level 4. URL: https://drafts.csswg.org/css-fonts-4/
[CSS-FONTS-5]
Chris Lilley. CSS Fonts Module Level 5. URL: https://drafts.csswg.org/css-fonts-5/
[TRUETYPE]
TrueType™ Reference Manual. URL: https://developer.apple.com/fonts/TrueType-Reference-Manual/
[WOFF]
Jonathan Kew; Tal Leming; Erik van Blokland. WOFF File Format 1.0. 13 December 2012. REC. URL: https://www.w3.org/TR/WOFF/

IDL 索引

[SecureContext]
partial interface Window {
  Promise<sequence<FontData>> queryLocalFonts(optional QueryOptions options = {});
};

dictionary QueryOptions {
  sequence<DOMString> postscriptNames;
};

[Exposed=Window]
interface FontData {
  Promise<Blob> blob();

  // Names
  readonly attribute USVString postscriptName;
  readonly attribute USVString fullName;
  readonly attribute USVString family;
  readonly attribute USVString style;
};

问题索引

虽然上方目标已提及对 Worker 的支持,目前该 API 按规范仅暴露于 Window 上下文。 ↵
移至 WindowOrWorkerGlobalScope 并理清权限相关问题。 ↵
考虑让 FontData 成为 可序列化对象,使 queryLocalFonts() 的结果可传递至 Worker。 ↵
记录除字符串本地化之外的国际化注意事项,例如 https://github.com/WICG/local-font-access/issues/72、https://github.com/WICG/local-font-access/issues/59 等。 ↵
是否应为 queryLocalFonts() 方法定义参数指定字符串目标语言(如 {lang: 'zh'}),未命中时回退至 `en-US`?或提供所有名称的访问,例如以 [BCP47] 语言标签为键的 name 映射?[Issue #69] ↵