自定义字体集

本主题介绍在应用中使用自定义字体的各种方法。

简介

大多数情况下,应用使用系统本地安装的字体。 DirectWrite使用 IDWriteFactory3::GetSystemFontSetIDWriteFactory::GetSystemFontCollection 方法提供对这些字体的访问权限。 在某些情况下,应用可能还希望使用作为Windows 10的一部分包含但当前未安装在当前系统上的字体。 可以使用 GetSystemFontSet 方法或调用 IDWriteFactory3::GetSystemFontCollection 并将 includeDownloadableFonts 设置为 TRUE,从 Windows 字体服务访问此类字体。 

但是,在某些应用程序方案中,应用需要使用未安装在系统中且未由 Windows 字体服务提供的字体。 下面是此类方案的示例:

  • 字体作为资源嵌入到应用二进制文件中。
  • 字体文件捆绑在应用包中,并存储在应用的安装文件夹下的磁盘上。
  • 应用是字体开发工具,需要加载用户指定的字体文件。 
  • 字体嵌入在可在应用中查看或编辑的文档文件中。 
  • 该应用使用从公共 Web 字体服务获取的字体。 
  • 该应用使用通过专用网络协议流式传输的字体数据。 

DirectWrite提供了用于在这些和其他类似方案中使用自定义字体的 API。 自定义字体数据可能来自本地文件系统中的文件;来自使用 HTTP 访问的基于云的远程源;加载到内存缓冲区后,或来自任意源。 

注意

虽然 DirectWrite 自 Windows 7 以来一直提供用于使用自定义字体的 API,但Windows 10以及Windows 10 创意者更新 (预览版 15021 或更高版本) 中添加了较新的 API,以便更轻松地实现上述几个方案。 本主题重点介绍窗口 10 中提供的 API。 有关需要在早期 Windows 版本上运行的应用程序,请参阅 Windows 7/8) (自定义字体集合 。 

 

API 摘要

本主题重点介绍以下 API 提供的功能:

 

关键概念

若要了解用于使用自定义字体DirectWrite API,了解这些 API 所依据的概念模型可能会有所帮助。 此处将介绍关键概念。 

当DirectWrite执行实际文本布局或呈现时,它需要访问实际字体数据。 字体面对象包含实际字体数据,这些数据必须存在于本地系统中。 但对于其他操作,例如检查特定字体的可用性,或向用户显示字体选择,只需要引用特定字体,而不是实际字体数据本身。 在 DirectWrite 中,字体面引用对象仅包含查找和实例化字体所需的信息。 由于字体人脸引用不保存实际数据,DirectWrite可以处理实际数据位于远程网络位置的字体人脸引用,以及实际数据在本地时。

字体集是一组字体引用,以及某些基本的信息属性,可用于引用字体或将其与其他字体进行比较,如姓氏或字体粗细值。 各种字体的实际数据可能是本地的,也可能都是远程的,也可能是混合的。

字体集可用于获取相应的字体集合对象。 有关更多详细信息,请参阅下面的字体集和字体集合。 

IDWriteFontSet 接口提供的方法允许查询属性值(如家族名称或字体粗细),或查询与特定属性值匹配的字体人脸引用。 在筛选到特定选定内容后,可以获取 IDWriteFontFaceReference 接口的实例,其中的方法用于下载 (如果实际字体数据当前是远程) ,则获取可用于布局和呈现的相应 IDWriteFontFace3 对象。 

IDWriteFontFile 界面是每个字体或字体面引用的依据。 这表示字体文件的位置,具有两个组件:字体文件加载器和字体文件键。 字体文件加载程序 (IDWriteFontFileLoader) 用于根据需要打开文件,并返回包含 IDWriteFontFileStream) (数据的流。 根据加载程序,数据可能位于本地文件路径、远程 URL或内存缓冲区中。 键是加载程序定义的值,用于唯一标识加载程序上下文中的文件,使加载程序能够查找数据并为其创建流。 

可以轻松地将自定义字体添加到自定义字体集,而自定义字体集又可用于筛选或组织字体信息,以用于创建字体选取器用户界面等目的。 字体集还可用于创建字体集合,以便在更高级别的 API(如 IDWriteTextFormatIDWriteTextLayout)中使用。 IDWriteFontSetBuilder 接口可用于创建包含多个自定义字体的自定义字体集。 它还可用于创建混合自定义字体和系统提供的字体的自定义字体集;或 ,将字体与实际数据的不同源(本地存储、远程 URL 和内存)混合使用。 

如前所述,字体人脸引用可能引用远程源中的字体数据,但数据必须是本地数据才能获取可用于布局和呈现的字体对象。 远程数据的下载由字体下载队列处理。 应用可以使用 IDWriteFontDownloadQueue 接口将下载远程字体的请求排入队列以启动下载过程,以及注册 IDWriteFontDownloadListener 对象以在下载过程完成时执行操作。 

对于此处所述的大多数接口,DirectWrite提供系统实现。 一个例外是 IDWriteFontDownloadListener 接口,当远程字体下载到本地时,应用将实现该接口以执行特定于应用的操作。 应用可能有理由为某些其他接口提供自己的自定义实现,但仅在特定、更高级的方案中才需要这些实现。 例如,应用需要提供 IDWriteFontFileLoader 接口的自定义实现,以处理使用 WOFF2 容器格式的本地存储中的字体文件。 下面将提供其他详细信息。 

字体和字体文件格式

另一个有助于理解的关键概念是各个字体面与包含它们的字体文件之间的关系。 OpenType 字体文件 (.ttf 或 .otf) 包含单个字体的想法是熟悉的。 但 OpenType 字体格式还允许 OpenType 字体集合 (.ttc 或 .otc) ,这是包含多个字体的单个文件。 OpenType 集合文件通常用于关系密切且对某些字体数据具有相同值的大型字体:通过将字体合并到单个文件中,可以删除重复的通用数据。 出于此原因,字体或字体人脸引用不仅需要引用字体文件 (或等效数据源) ,而且还必须在该文件中指定字体索引,一般情况下,该文件可能是集合文件。 

对于 Web 上使用的字体,字体数据通常打包为某些容器格式(WOFF 或 WOFF2),这些格式提供字体数据的一些压缩,并针对盗版和违反字体许可证提供某种级别的保护。 从功能上说,WOFF 或 WOFF2 文件等效于 OpenType 字体或字体集合文件,但数据采用不同的格式编码,需要先解压缩才能使用。 

某些DirectWrite API 可以处理单个字体,而其他 API 可以处理可能包含包含多个人脸的 OpenType 集合文件的文件。 同样,某些 API 仅处理原始 OpenType 格式数据,而其他 API 可以处理打包的 WOFF 和 WOFF2 容器格式。 以下讨论中提供了这些详细信息。 

字体集和字体集合

某些应用程序可能实现为使用 IDWriteFontCollection 接口来处理字体。 字体集合和字体集之间存在直接的对应关系。 每个字体可以保留相同的字体,但它们向不同的组织呈现它们。 可以从任何字体集合中获取相应的字体集,反之亦然。

使用许多自定义字体时,最简单的方法是使用字体集生成器界面创建自定义字体集,然后在创建字体集后获取字体集合。 下面将详细介绍创建自定义字体集的过程。 若要从字体集中获取 IDWriteFontCollection1 接口,请使用 IDWriteFactory3::CreateFontCollectionFromFontSet 方法。

如果应用具有集合对象并且需要获取相应的字体集,则可以使用 IDWriteFontCollection1::GetFontSet 方法完成此操作。 

常见方案

本部分介绍涉及自定义字体集的一些最常见方案:

  • 在本地文件系统的路径上使用任意字体创建自定义字体集。
  • 使用已知字体创建自定义字体集 (可能与存储在本地文件系统中的应用) 捆绑在一起。
  • 在 Web 上使用已知的远程字体创建自定义字体集。
  • 使用加载到内存中的字体数据创建自定义字体集。

DirectWrite自定义字体集示例中提供了这些方案的完整实现。 该示例还演示了用于处理以 WOFF 或 WOFF2 容器格式打包的字体数据的更高级方案,这将在下面讨论。 

在本地文件系统中使用任意字体创建字体集

在本地存储中处理任意一组字体文件时, IDWriteFontSetBuilder1::AddFontFile 方法很方便,因为在单个调用中,它可以处理 OpenType 字体集合文件中的所有字体,以及 OpenType 变量字体的所有实例。 这在Windows 10 创意者更新 (预览版 15021 或更高版本) 中可用,建议在可用时使用。 

若要使用此方法,请使用以下过程。

1. 首先创建 IDWriteFactory5 接口:
IDWriteFactory5* pDWriteFactory; 
HRESULT hr = DWriteCreateFactory( 
  DWRITE_FACTORY_TYPE_SHARED, 
  __uuidof(IDWriteFactory5), 
  reinterpret_cast<IUnknown**>(&pDWriteFactory) 
); 

 
2. 使用工厂获取 IDWriteFontSetBuilder1 接口:

IDWriteFontSetBuilder1* pFontSetBuilder; 
if (SUCCEEDED(hr)) 
{ 
  hr = pDWriteFactory->CreateFontSetBuilder(&pFontSetBuilder); 
}  
                
  1. 对于本地文件系统中的每个字体文件,请创建一个引用它的 IDWriteFontFile
IDWriteFontFile* pFontFile; 
if (SUCCEEDED(hr)) 
{ 
  hr = pDWriteFactory->CreateFontFileReference(pFilePath, /* lastWriteTime*/ nullptr, &pFontFile); 
} 

 
4. 使用 AddFontFile 方法将 IDWriteFontFile 对象添加到字体集生成器:

hr = pFontSetBuilder->AddFontFile(pFontFile); 

如果在对 CreateFontFileReference 的调用中指定的文件路径引用了受支持的 OpenType 文件以外的内容,则对 AddFontFile 的调用将返回错误,DWRITE_E_FILEFORMAT。

  1. 将所有文件添加到字体集生成器后,可以创建自定义字体集:
IDWriteFontSet* pFontSet; 
hr = pFontSetBuilder->CreateFontSet(&pFontSet); 

 

如果应用需要在低于 Windows 10 创意者更新 Windows 10 个版本上运行,则 AddFontFile 方法将不可用。 可以通过创建 IDWriteFactory3 接口并使用 QueryInterface 尝试获取 IDWriteFactory5 接口来检测可用性:如果成功,则 IDWriteFontSetBuilder1 接口和 AddFontFile 方法也将可用。

如果 AddFontFile 方法不可用,则必须使用 IDWriteFontSetBuilder::AddFontFaceReference 方法添加单个字体。 若要允许包含多个人脸的 OpenType 字体集合文件,可以使用 IDWriteFontFile::Analyze 方法来确定文件中包含的人脸数。 此过程如下所示。

1. 首先创建 IDWriteFactory3 接口:
IDWriteFactory3* pDWriteFactory; 
HRESULT hr = DWriteCreateFactory( 
DWRITE_FACTORY_TYPE_SHARED, 
  __uuidof(IDWriteFactory5), 
  reinterpret_cast<IUnknown**>(&pDWriteFactory) 
); 
  1. 使用工厂获取 IDWriteFontSetBuilder 接口:
IDWriteFontSetBuilder* pFontSetBuilder; 
if (SUCCEEDED(hr)) 
{ 
  hr = pDWriteFactory->CreateFontSetBuilder(&pFontSetBuilder); 
} 
  1. 对于每个字体文件,创建 一个 IDWriteFontFile,如下所示:
IDWriteFontFile* pFontFile; 
if (SUCCEEDED(hr)) 
{ 
  hr = pDWriteFactory->CreateFontFileReference(pFilePath, /* lastWriteTime*/ nullptr, &pFontFile); 
} 

我们需要确定人脸的数量并创建单独的 IDWriteFontFaceReference 对象,而不是将文件直接添加到字体集生成器。 
4. 使用 Analyze 方法获取文件中的人脸数。 

BOOL isSupported; 
DWRITE_FONT_FILE_TYPE fileType; 
UINT32 numberOfFonts; 
hr = pFontFile->Analyze(&isSupported, &fileType, /* face type */ nullptr, &numberOfFonts); 

Analyze 方法还将设置 isSupported 和 fileType 参数的值。 如果文件不是受支持的格式,则 isSupported 将为 FALSE,并且可以采取适当的操作,例如忽略文件。 
5. 循环访问 numberOfFonts 参数中设置的字体数。 在 循环中,为每个文件/索引对创建 IDWriteFontFaceReference ,并将其添加到字体集生成器。 

for (uint32_t fontIndex = 0; fontIndex < numberOfFonts; fontIndex++) 
{ 
  IDWriteFontFaceReference* pFontFaceReference;
  hr = pDWriteFactory->CreateFontFaceReference(pFontFile, fontIndex, DWRITE_FONT_SIMULATIONS_NONE, &pFontFaceReference);

  if (SUCCEEDED(hr))
  {
    hr = pFontSetBuilder->AddFontFaceReference(pFontFaceReference);
  }
} 
  1. 将所有人脸添加到字体集生成器后,创建自定义字体集,如上所示。

可以将应用设计为在 Windows 10 创意者更新 上运行时使用首选的 AddFontFile 方法,但在早期Windows 10版本上运行时回退到使用 AddFontFaceReference 方法。 如上所述测试 IDWriteFactory5 接口的可用性,然后相应地分支。 此方法在DirectWrite自定义字体集示例中进行了说明。 

在本地文件系统中使用已知字体创建字体集

如上所述,字体集中的每个字体人脸引用都与某些信息属性相关联,例如姓氏和字体粗细。 使用上面列出的 API 调用将自定义字体添加到字体集生成器时,将直接从实际字体数据获取这些信息属性,该数据在添加字体时读取。 但是,在某些情况下,如果应用具有其他有关字体的信息源,则它可能希望为这些属性提供自己的自定义值。 

例如,假设某个应用捆绑了一些字体,这些字体用于在应用中显示特定的用户界面元素。 有时,例如使用新的应用版本,应用用于这些元素的特定字体可能需要更改。 如果应用对特定字体的引用进行了编码,则将一种字体替换为另一种字体将需要更改其中每个引用。 相反,如果应用使用自定义属性根据要呈现的元素或文本的类型分配功能别名,则将每个别名映射到一个位置的特定字体,然后在创建和操作字体的所有上下文中使用别名,那么将一种字体替换为另一种字体只需更改别名映射到特定字体的一个位置。 

在调用 IDWriteFontSetBuilder::AddFontFaceReference 方法时,可以分配信息属性的自定义值。 执行此操作的方法如下:这可用于任何Windows 10版本。 

如上所示,首先获取 IDWriteFactory3IDWriteFontSet 接口。 对于要添加的每个自定义字体,请创建一个 IDWriteFontFaceReference,如上所示。 但是,在步骤 5 的循环中将其添加到字体集生成器 ((如上) 所示)之前,应用会定义要使用的自定义属性值。 

一组自定义属性值是使用 DWRITE_FONT_PROPERTY 结构的数组定义的。 其中每个都标识 DWRITE_FONT_PROPERTY_ID 枚举中的一个特定属性,以及要使用的相应属性值。  

请注意,所有属性值都分配为字符串。 如果以后可能会向用户显示这些值,则可以设置不同语言的给定属性的备用值,但这不是必需的。 另请注意,如果应用设置了任何自定义属性值,则字体集中仅使用指定的这些值;对于字体集中使用的信息属性,DirectWrite不会直接从字体派生任何值。 

以下示例定义了三个信息属性的自定义值:姓氏、全名和字体粗细。 

DWRITE_FONT_PROPERTY props[] = 
{ 
  { DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, L"My Icon Font", L"en-US" }, 
  { DWRITE_FONT_PROPERTY_ID_FULL_NAME, L"My Icon Font", L"en-US" }, 
  { DWRITE_FONT_PROPERTY_ID_WEIGHT, L"400", nullptr } 
}; 
               
            

定义字体的所需属性值数组后,调用 AddFontFaceRefence,传递属性数组以及字体人脸引用。 

hr = pFontSetBuilder->AddFontFaceReference(pFontFaceReference, props, ARRAYSIZE(props)); 

 

将所有自定义字体面添加到字体集生成器及其自定义属性后,请创建自定义字体集,如上所示。 

在 Web 上使用已知的远程字体创建自定义字体集

自定义属性对于使用远程字体非常重要。 每个字体人脸引用都必须具有一些信息属性来描述字体的特征,并将其与其他字体区分开来。 由于远程字体的字体数据不是本地字体,DirectWrite无法直接从字体数据派生属性。 因此,向字体集生成器添加远程字体时,必须显式提供属性。

将远程字体添加到字体集的 API 调用序列类似于前面方案所述的序列。 但是,由于字体数据是远程的,因此读取实际字体数据所涉及的操作将不同于使用本地存储中的文件时。 对于这种情况,已在Windows 10 创意者更新中添加了一个新的较低级别的接口 IDWriteRemoteFontFileLoader。 

若要使用远程字体文件加载程序,必须先将其注册到DirectWrite工厂。 只要正在使用与加载程序关联的字体,应用就需要保留加载程序。 一旦字体不再使用,并在工厂销毁之前的某个时间点,必须取消注册加载程序。 这可以在拥有加载程序对象的类的析构函数中完成。 这些步骤如下所示。 

使用远程字体创建自定义字体集的方法如下:这需要Windows 10 创意者更新。  

1. 创建 IDWriteFactory5 接口,如上所示。  2. 创建 IDWriteFontSetBuilder 接口,如上所示。  3. 使用工厂获取 IDWriteRemoteFontFileLoader。 
IDWriteRemoteFontFileLoader* pRemoteFontFileLoader; 
if (SUCCEEDED(hr)) 
{ 
    hr = pDWriteFactory->CreateHttpFontFileLoader( 
        /* referrerURL */ nullptr, 
        /* extraHeaders */ nullptr, 
        &pRemoteFontFileLoader 
    ); 
} 

这将返回系统提供的远程字体文件加载程序接口的实现,该接口能够处理 HTTP 交互以代表应用下载字体数据。 如果作为字体源的字体服务需要,可以指定引用 URL 或额外的标头。  

重要

安全说明:尝试提取远程字体时,攻击者可能会欺骗将要调用的目标服务器。 在这种情况下,目标、引用网站的 URL 和标头详细信息将泄露给攻击者。 应用开发人员负责缓解此风险。 建议使用 HTTPS 协议,而不是 HTTP。 

 

单个远程字体文件加载程序可用于多个字体,但如果从对引荐 URL 或额外标头有不同要求的多个服务获取字体,则可以使用不同的加载程序。 
4. 向工厂注册远程字体文件加载程序。 

 if (SUCCEEDED(hr)) 
 { 
     hr = pDWriteFactory->RegisterFontFileLoader(pRemoteFontFileLoader); 
 } 

此时,创建自定义字体集的步骤与已知本地字体文件中所述的步骤类似,但有两个重要例外。 首先, IDWriteFontFile 对象是使用远程字体文件加载程序接口而不是工厂创建的。 其次,不能使用 Analyze 方法,因为字体数据不是本地的。 相反,应用必须知道远程字体文件是否为 OpenType 字体集合文件,如果是,则它必须知道它将使用集合中的哪些字体,以及每个字体的索引。 因此,其余步骤如下所示。 
5. 对于每个远程字体文件,请使用远程字体文件加载程序接口创建 IDWriteFontFile,并指定访问字体文件所需的 URL。 

 IDWriteFontFile* pFontFile; 
 hr = pRemoteFontFileLoader->CreateFontFileReferenceFromUrl( 
     pDWriteFactory, 
     /* baseUrl */ L"https://github.com/", 
     /* fontFileUrl */ L"winjs/winjs/blob/master/src/fonts/Symbols.ttf?raw=true", 
     &pFontFile 
 ); 

请注意,可以在 fontFileUrl 参数中指定完整的 URL,也可以将其拆分为基部分和相对部分。 如果指定了基 URL,则 baseUrl 和 fontFileUrl 值的串联必须提供完整的 URL - DirectWrite不会提供任何其他分隔符。

重要

安全/性能说明:尝试提取远程字体时,无法保证 Windows 会收到来自服务器的响应。 在某些情况下,对于无效的相对 URL,服务器可能会响应“找不到文件”错误,但如果收到多个无效请求,服务器会停止响应。 如果服务器不响应,Windows 最终将超时,但如果启动多个提取,则可能需要几分钟时间。 应尽你所能确保 URL 在进行调用时有效。 

 

另请注意,URL 可以指向原始 OpenType 字体文件 (.ttf、.otf、.ttc、.otc) ,但它也可以指向 WOFF 或 WOFF2 容器文件中的字体。 如果引用 WOFF 或 WOFF2 文件,则远程字体文件加载程序DirectWrite实现将自动从容器文件解包字体数据。 
6. 对于要使用的远程字体文件中的每个字体人脸索引,创建 IDWriteFontFaceReference。 

 IDWriteFontFaceReference* pFontFaceReference; 
 hr = pDWriteFactory->CreateFontFaceReference(pFontFile, /* faceIndex */ 0, DWRITE_FONT_SIMULATIONS_NONE, &pFontFaceReference);
  1. 定义字体的自定义属性,如上所示。 
  2. 将字体面引用以及自定义属性添加到字体集生成器,如上所示。 
  3. 将所有字体添加到字体集生成器后,创建字体集,如上所示。 
  4. 当不再使用远程字体时,请注销远程字体文件加载程序。 
hr = pDWriteFactory->UnregisterFontFileLoader(pRemoteFontFileLoader); 

创建具有自定义远程字体的自定义字体集后,该字体集将包含远程字体的引用和信息属性,但实际数据仍为远程字体。 DirectWrite对远程字体的支持允许在字体集中保留字体引用,并选择字体以用于布局和呈现,但在实际需要使用它之前不会下载实际数据,例如何时执行文本布局。  

应用可以采用一种预先方法,方法是请求DirectWrite下载字体数据,然后等待确认成功下载,然后再开始对字体进行任何处理。 但网络下载意味着一些不可预知的持续时间延迟,并且成功也是不确定的。 因此,通常最好采用不同的方法,允许最初使用已是本地的备用或回退字体完成布局和呈现,同时并行请求下载所需的远程字体,然后在下载所需字体后更新结果。 

若要请求在使用之前下载整个字体,可以使用 IDWriteFontFaceReference::EnqueueFontDownloadRequest 方法。 如果字体非常大,则处理特定字符串可能只需要一部分数据。 DirectWrite提供了其他方法,可用于请求特定内容所需的字体数据部分,EnqueueCharacterDownloadRequestEnqueueGlyphDownloadRequest。  

假设要在应用中采用的方法是允许最初使用本地、备用或回退字体完成处理。 IDWriteFontFallback::MapCharacters 方法可用于标识本地回退字体,它还会自动将下载首选字体的请求排入队列。 此外,如果使用 IDWriteTextLayout,并且布局中的部分或所有文本是使用远程字体引用设置格式的,则 DirectWrite 将自动使用 MapCharacters 方法获取本地回退字体并排队请求下载远程字体数据。 

DirectWrite维护每个工厂的字体下载队列,使用上述方法发出的请求将添加到该队列。 可以使用 IDWriteFactory3::GetFontDownloadQueue 方法获取字体下载队列。 

如果发出下载请求,但字体数据已是本地字体数据,则会导致无操作:不会向下载队列添加任何内容。 应用可以通过调用 IDWriteFontDownloadQueue::IsEmpty 方法检查队列是否为空或存在挂起的下载请求。 

将远程字体请求添加到队列后,必须启动下载过程。 在 IDWriteTextLayout 中使用远程字体时,当应用调用强制布局或呈现操作的 IDWriteTextLayout 方法(如 GetLineMetrics 或 Draw 方法)时,将自动启动下载。 在其他方案中,应用必须通过调用 IDWriteFontDownloadQueue::BeginDownload 直接启动下载。  

下载完成后,由应用采取适当的操作-继续执行挂起的操作,或重复操作,最初使用回退字体执行。 (如果使用DirectWrite的文本布局,则可以使用 IDWriteTextLayout3::InvalidateLayout 清除使用回退 fonts.) 为使应用在下载过程完成时收到通知并采取相应操作,应用必须提供 IDWriteFontDownloadListener 接口的实现,并将其传递到 BeginDownload 调用中。 

重要

安全/性能说明:尝试提取远程字体时,无法保证 Windows 会收到来自服务器的响应。 如果服务器不响应,Windows 最终将超时,但如果提取多个远程字体但失败,则可能需要几分钟时间。 BeginDownload 调用将立即返回。 应用不应在等待 调用 IDWriteFontDownloadListener::D ownloadCompleted 时阻止 UI。 

 

这些与 DirectWrite 的字体下载队列和 IDWriteFontDownloadListener 接口交互的示例实现可在DirectWrite自定义字体集示例以及DirectWrite可下载字体示例中查看。 

使用加载到内存中的字体数据创建自定义字体集

正如从字体文件读取数据的低级别操作对于本地磁盘上的文件与 Web 上的远程文件不同一样,加载到内存缓冲区中的字体数据也是如此。 Windows 10 创意者更新 IDWriteInMemoryFontFileLoader 中添加了用于处理内存中字体数据的新低级别接口。 

与远程字体文件加载程序一样,必须首先向DirectWrite工厂注册内存中的字体文件加载程序。 只要正在使用与加载程序关联的字体,应用就会需要保留该加载程序。 一旦字体不再使用,并在工厂销毁之前的某个时间点,必须取消注册加载程序。 这可以在拥有加载程序对象的类的析构函数中完成。 这些步骤如下所示。 

如果应用具有由数据表示的字体面的单独信息,则它可以向字体集生成器添加单个字体面引用,并指定了自定义属性。 但是,由于字体数据位于本地内存中,因此这不是必需的;DirectWrite将能够直接读取数据以派生属性值。 

DirectWrite假定字体数据采用原始 OpenType 格式,等效于 openType 文件 (.ttf、.otf、.ttc、.otc) ,但在内存中而不是磁盘上。 数据不能采用 WOFF 或 WOFF2 容器格式。 数据可以表示 OpenType 字体集合。 如果未使用自定义属性,则可以使用 IDWriteFontSetBuilder1::AddFontFile 方法在单个调用中添加数据中的所有字体。 

内存中方案的一个重要考虑因素是数据的生存期。 如果向DirectWrite提供了指向缓冲区的指针,而没有明确指示存在所有者,则DirectWrite会将数据复制到它将拥有的新内存缓冲区中。 为了避免复制数据和其他内存分配,应用可以传递实现 IUnknown 且拥有包含字体数据的内存缓冲区的数据所有者对象。 通过实现此接口,DirectWrite可以添加到对象的引用计数中,从而确保拥有数据的生存期。 

使用内存中字体数据创建自定义字体集的方法如下:这需要Windows 10 创意者更新。 这将假定应用实现的数据所有者对象,该对象实现 IUnknown,并且还具有返回指向内存缓冲区和缓冲区大小的指针的方法。 

1. 创建 IDWriteFactory5 接口,如上所示。 2. 创建 [**IDWriteFontSetBuilder1**] (/windows/win32/api/dwrite_3/nn-dwrite_3-idwritefontsetbuilder1) 接口,如上所示。 3. 使用工厂获取 IDWriteInMemoryFontFileLoader。 
 IDWriteInMemoryFontFileLoader* pInMemoryFontFileLoader; 
if (SUCCEEDED(hr)) 
{ 
    hr = pDWriteFactory->CreateInMemoryFontFileLoader(&pInMemoryFontFileLoader); 
}

这将返回系统提供的内存中字体文件加载程序接口的实现。 
4. 将内存中字体文件加载程序注册到工厂。 

if (SUCCEEDED(hr)) 
{ 
    hr = pDWriteFactory->RegisterFontFileLoader(pInMemoryFontFileLoader); 
}

 
5. 对于每个内存中字体文件,使用内存中字体文件加载程序创建 IDWriteFontFile。 

IDWriteFontFile* pFontFile; 
hr = pInMemoryFontFileLoader->CreateInMemoryFontFileReference( 
    pDWriteFactory, 
    pFontDataOwner->fontData /* returns void* */, 
    pFontDataOwner->fontDataSize /* returns UINT32 */, 
    pFontDataOwner /* ownerObject, owns the memory with font data and implements IUnknown */, 
    &pFontFile 
); 

 
6. 使用 AddFontFile 方法将 IDWriteFontFile 对象添加到字体集生成器,如上所示。  如果需要,应用可以基于 IDWriteFontFile 创建单个 IDWriteFontFaceReference 对象,可以选择为每个字体面引用定义自定义属性,然后使用 AddFontFaceReference 方法将具有自定义属性的字体面引用添加到字体集,如上所示。 
7. 将所有字体添加到字体集生成器后,创建自定义字体集,如上所示。 
8. 在不再使用内存中字体的某个时刻,请注销内存中字体文件加载程序。 

hr = pDWriteFactory->UnregisterFontFileLoader(pInMemoryFontFileLoader);

 

高级方案

某些应用可能具有特殊要求,需要比上述更高级的处理。 

合并字体集

某些应用可能需要创建一个字体集,该字体集包含来自其他字体集的项目的某种组合。 例如,应用可能需要创建一个字体集,该字体集将系统上安装的所有字体与所选的自定义字体组合在一起,或者将符合特定条件的已安装字体与其他字体组合在一起。 DirectWrite具有支持字体集操作和组合的 API。 

若要合并两个或多个字体集, IDWriteFontSetBuilder::AddFontSet 方法会将给定字体集中的所有字体添加到单个调用中要添加到字体集生成器中。 如果新字体集中仅需要现有字体集中的某些字体,则可以使用 IDWriteFontSet::GetMatchingFonts 方法派生已筛选为仅包含与指定属性匹配的字体的新字体集对象。 这些方法提供了一种简单的方法来创建自定义字体集,这些字体集合并了两个或更多个现有字体集

使用本地 WOFF 或 WOFF2 字体数据

如果应用在本地文件系统或内存缓冲区中具有字体文件,但它们使用 WOFF 或 WOFF2 容器格式,DirectWrite (Windows 10 Creator Update 或更高版本) 提供了用于解压缩容器格式的 IDWriteFactory5::UnpackFontFile 的方法,它将返回 IDWriteFontFileStream。 

但是,应用需要一种方法来将 IDWriteFontFileStream 放入字体文件加载器对象中。 执行此操作的一种方法是创建包装流的自定义 IDWriteFontFileLoader 实现。 与其他字体文件加载程序一样,必须在使用前进行注册,并在工厂超出范围之前取消注册。  

如果自定义加载程序还将用于未打包) 字体文件的原始 (,则应用还需要提供用于处理这些文件的 IDWriteFontFileStream 接口的自定义实现。 不过,使用上面讨论的 API 处理原始字体文件的方法更简单。 可以通过对打包字体文件与原始字体文件使用单独的代码路径来避免对自定义流实现的需求。 

创建自定义字体文件加载程序对象后,打包的字体文件数据将通过特定于应用的方式添加到加载程序。 加载程序可以处理多个字体文件,其中每个字体文件都是使用应用定义的键标识的,该键对DirectWrite不透明。 将打包的字体文件添加到加载程序后, IDWriteFactory::CreateCustomFontFileReference 方法用于根据给定键标识的字体数据的加载程序获取 IDWriteFontFile 。  

字体数据的实际解压缩可以在字体添加到加载程序时完成,但也可在 IDWriteFontFileLoader::CreateStreamFromKey 方法中进行处理,DirectWrite在首次需要读取字体数据时调用此方法。 

创建 IDWriteFontFile 对象后,将字体添加到自定义字体集的剩余步骤将如上所述。 

DirectWrite自定义字体集示例中演示了使用此方法的实现。 

将DirectWrite远程字体机制与自定义低级别网络实现配合使用

用于处理远程字体的DirectWrite机制可分为更高级别的机制(具有包含远程字体的字体面引用的字体集、检查字体数据的区域和管理字体下载请求的队列)和处理实际下载的较低级别机制。 某些应用可能想要利用更高级别的远程字体机制,但也需要自定义网络交互,例如使用 HTTP 以外的协议与服务器通信。 

在这种情况下,应用需要创建 IDWriteRemoteFontFileLoader 接口的自定义实现,该接口以所需方式与其他较低级别的接口交互。 应用还需要提供这些较低级别接口的自定义实现: IDWriteRemoteFontFileStreamIDWriteAsyncResult。 这三个接口具有DirectWrite将在下载操作期间调用的回调方法。 

调用 IDWriteFontDownloadQueue::BeginDownload 时,DirectWrite将向远程字体文件加载程序查询数据的区域,并请求远程流。 如果数据不是本地数据,则它将调用流的 BeginDownload 方法。 流实现不应阻止该调用,但应立即返回,传递回 IDWriteAsyncResult 对象,该对象提供等待句柄DirectWrite将用于等待异步下载操作。 自定义流实现负责处理远程通信。 完成事件发生后,DirectWrite将调用 IDWriteAsyncResult::GetResult 来确定操作的结果。 如果结果成功,则预计后续对下载范围的流的 ReadFragment 调用会成功。 

重要

安全/性能说明:尝试提取远程字体时,攻击者通常会欺骗所调用的目标服务器,或者服务器可能无法响应。 如果要实现自定义网络交互,则与处理第三方服务器时相比,可以更好地控制缓解措施。 但是,由你来考虑适当的缓解措施,以避免信息泄露或拒绝服务。 建议使用 HTTPS 等安全协议。 此外,还应在某个超时时间生成,以便最终设置返回到 DirectWrite 的事件句柄。 

 

早期 Windows 版本的支持方案

在早期版本的 Windows 上,DirectWrite中可以支持所述的方案,但需要使用Windows 10之前提供的更有限的 API 在应用中需要更多自定义实现。 有关详细信息,请参阅 自定义字体集合 (Windows 7/8)