DirectX 因素

谁是害怕的标志符号运行?

Charles Petzold

下载代码示例

我第一次遇到几年前字符标志符号在 Windows 的演示文稿基础 (WPF) 的概念。排版,在"字形"是指一个字符,而不是其功能的特定的书面语言,由字符的 Unicode 值表示的图形表示形式。我知道 WPF 字形元素和 GlyphRun 类表示另一种方法来显示文本,但他们似乎添加一个图层的不必要的复杂性。最大的奇异涉及引用所需的字体,不由一个字体系列名称,但由实际字体文件的文件路径。我已经被编码为 Windows 自 beta 版本的 1.0,和以前从未不得不由文件名引用的字体。它根本就不这样。

我迅速得出结论字形和 GlyphRun 是多太深奥为主流的程序员,包括我自己。仅几年后当我开始工作与 Microsoft XML 纸张规范 (XPS) 没有明显的标志符号背后的理念。

基本的区别

通常当程序请求从 Windows 的一种字体,它会使用如世纪教科书和属性如斜体或加粗的字体系列名称。在运行时,Windows 发现与字体家族名称相匹配的用户的系统上的字体文件。斜体和粗体或者可以是固有的这种字体,或由系统合成。文本的布局,Windows 或应用程序访问字体度量信息,描述字体的字符的大小。

如果两个不同的用户有两个不同的字体文件都包含带有家族名称世纪教科书,但有些不同字体度量标准的字体在系统上时,会发生什么呢?这不是一个真正的问题。因为文本是格式和布局在运行时,这些两个字体的实际呈现可能略有不同,但它真的不重要。应用程序被设计为以这种方式灵活。

XPS 文档是不同的然而。多像 PDF、 XPS 描述一个固定页的文档。每个页面上的所有文本是已经规定,准确地定位。如果这种文件在呈现时的字体,用来设计页面的字体稍有不同,不会看起来很正确。这就是为什么 XPS 文档通常包含嵌入的字体文件,以及为什么 XPS 页使用的标志符号元素来引用这些字体文件并呈现文本。完全消除任何含糊之处。

通常我们提到的文本字符的字符代码。我们通常不需要知道的事的发生,由于采用了与特定字体的字符代码显示的精确标志符号。但有时它是重要的是要有更好地控制自己的标志符号。

您可能认为有一对一的对应关系之间的字符代码和特定字体中的字形和通常是这样。但也有非常重要的例外情况:某些字体文件包含连笔字,是对应于字符对,如 fi 或 fl 的单个符号。一些非拉丁字符集需要多个字形要呈现的单个字符。此外,某些字体文件包含某些字符替代字形 — — 例如,一个以通过它,小型大写的字母为小写或字母与文体花饰字使用斜杠零。这些文体的替代品是一个特定的字体文件,而不是一种字体的特点。这就是为什么获取正确的字体文件是关键。

如果您编写 Windows 8 的一个 Windows 应用商店的应用程序,您可以使用 Windows 运行时 API 通过这些文体的替代品。"编程窗口,第 6 版"的第 16 章 (O'Reilly 媒体,2012年) 有一个项目叫 TypographyDemo,演示如何做到这一点。但在 Windows 8 字形的基础支持在 DirectX,实施的字形本身力量透露最在该环境中。

DirectX 标志符号支持

探索如何在 DirectX 中显示字形涉及跟踪路径通过几个接口、 方法和结构。

让我们开始与 ID2D1RenderTarget 接口,其中包含要显示 2D 图形和文本的基本方法。此接口定义了一个 DrawGlyphRun 方法。这种方法需要一个结构类型描述要显示的标志符号的 DWRITE_GLYPH_RUN 和对象的引用也键入 IDWriteFontFace。

获取一个 IDWriteFontFace 对象的一种方法是通过 IDWriteFactory 的 CreateFontFace 方法。这种方法需要一个 IDWriteFontFile 对象,它通过 IDWriteFactory 的 CreateFontFileReference 方法获得的。

CreateFontFileReference 方法引用字体文件使用的路径和文件名。然而,如果使用 DirectX 在 Windows 存储应用程序中,您的应用程序可能不会有全面和自由进入到用户的硬盘上,并通常不能访问字体以这种方式。你用 CreateFontFileReference 引用任何字体文件将有可能被定义为应用程序资源,并将绑定到应用程序的包。

然而,您不能在您的应用程序的包中包括只是任何字体文件。如果字体是您的应用程序的一部分,您必须将分发该字体的许可证。幸运的是,微软已许可从字母上伸部分公司的几个字体文件。为使您可以将它们与应用程序一起分发的明确目的。这些字体文件已主要用于 XNA 应用程序,但是他们也有趣从字形的角度。

此列的可下载代码之间是一个程序,称为 GlyphDump,我创建了基于 DirectX App (XAML) 模板在 Visual Studio 表示 2013年预览中。此模板创建的应用程序呈现上 SwapChainPanel 的 DirectX 图形。

我在命名的字体的 GlyphDump 项目中创建新的筛选器 (和相应的文件夹中),从字母上伸部分公司领有牌照的 10 字体文件添加在 DirectXPage.xaml 中的列表框显式列出的这些字体家族名称与标记属性引用的文件名。当其中一个选择时,该程序构造的路径和名称的字体文件就像这样:

 

Package::Current->InstalledLocation->Path +
  "\\Fonts\\" + filename;

GlyphDumpRenderer 类然后使用该路径创建一个 IDWriteFontFile 对象和一个 IDWriteFontFace 对象。

其他的工作发生在 Render 方法中,如中所示图 1

图 1 中 GlyphDump 的渲染方法

bool GlyphDumpRenderer::Render()
{
  if (!m_needsRedraw)
      return false;
  ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::White));
  context->SetTransform(m_deviceResources->GetOrientationTransform2D());
  if (m_fontFace != nullptr)
  {
    Windows::Foundation::Size outputBounds = m_deviceResources->GetOutputBounds();
    uint16 glyphCount = m_fontFace->GetGlyphCount();
    int rows = (glyphCount + 16) / 16;
    float boxHeight = outputBounds.Height / (rows + 1);
    float boxWidth = outputBounds.Width / 17;
    // Define entire structure except glyphIndices
    DWRITE_GLYPH_RUN glyphRun;
    glyphRun.fontFace = m_fontFace.Get();
    glyphRun.fontEmSize = 0.75f * min(boxHeight, boxWidth);
    glyphRun.glyphCount = 1;
    glyphRun.glyphAdvances = nullptr;
    glyphRun.glyphOffsets = nullptr;
    glyphRun.isSideways = false;
    glyphRun.bidiLevel = 0;
    for (uint16 index = 0; index < glyphCount; index++)
    {
      glyphRun.glyphIndices = &index;
      context->DrawGlyphRun(Point2F(((index) % 16 + 0.5f) * boxWidth,
                                    ((index) / 16 + 1) * boxHeight),
                                    &glyphRun,
                                    m_blackBrush.Get());
    }
  }
  // We ignore D2DERR_RECREATE_TARGET here. This error indicates
  // that the device is lost. It will be handled during
  // the next call to Present.
  HRESULT hr = context->EndDraw();
  if (hr != D2DERR_RECREATE_TARGET)
  {
    DX::ThrowIfFailed(hr);
  }
  context->RestoreDrawingState(m_stateBlock.Get());
  m_needsRedraw = false;
  return true;
}

此渲染方法显示所有的标志符号中的所选的字体文件中每个 16 标志符号的行所以它尝试计算足够小,显示的 fontEmSize 值。 (对于一些与很多字形的字体,这不会工作很好)。DWRITE_GLYPH_RUN 结构的 glyphIndices 字段通常的标志符号索引数组。 在这里我仅要一次显示一个标志符号。

DrawGlyphRun 方法需要一个坐标点、 DWRITE_GLYPH_RUN 结构和画笔。 坐标指示标志符号的基线的左边的缘在哪里进行定位。 注意我说的"基线"。这是不同于大多数文本显示方法,需要指定左上角的第一个字符。

图 2 显示也许最有趣的这批,有一个家族名称的佩斯卡德罗和 Pesca.ttf 字体文件名的字体。

The GlyphDump Program Showing the Pescadero Font
图 2 显示佩斯卡德罗字体,GlyphDump 程序

在编写此程序之前, 我从未见过像这样的显示。 它开始看起来有点像一个传统的 ASCII 表,但是然后有一堆的数字下标和上标,和整个集合的各种排序,再加上大写字母与装饰花饰字连笔字的字形。

很明显,DWRITE_GLYPH_RUN 的 glyphIndices 字段不是一个字符代码相同。 这些都是引用的字体文件中实际的字型的指数,这些标志符号甚至不需要在任何一种理性的秩序。

标志符号的文本

您可能已经看到一些实用程序中能够显示使用特定字体文件标志符号索引,而不是字符代码的文本。 你可以指定你想要的到底是什么标志符号。

FancyTitle 项目演示此操作。 这里的想法是你有一个程序,主要是基于 XAML,但你想要一个花哨的标题从佩斯卡德罗字体使用的一些连字和花饰字。 该项目包括的 Pesca.ttf 文件和 XAML 文件定义了 SwapChainPanel 有的 778 的宽度和高度 54,我选择了经验主义地基于中呈现的文本的大小的值。

因为这一项目的显示要求很简单,但我删除的 FancyTitleMain 类和呈现类,离开要呈现到 SwapChainPanel 的 DirectXPage 类。 (不过,我不得不修改 DeviceResources 略,使 IDeviceNotify 一个 ref 类接口 DirectXPage 可以实现设备) 时­通知时的输出设备是丢失了,重新创建通知.)

中显示的文本输出图 3 具有 24 个字符,但只有 22 标志符号。 你就会认识到连字和花饰字从图 2

The FancyTitle Output
图 3 FancyTitle 输出

我做了 DirectX 背景爱丽丝蓝色,所以你可以看到 SwapChainPanel 是几乎不大于文本。 当然,它是可能预测输出的大小,正是因为该字体文件是应用程序的一部分和标志符号,将被直接访问。

你可以连字和替代字形,而无需使用 DrawGlyphRun。 你也可以让他们在 Windows 运行时,使用的 TextBlock 元素作为印刷术在精度较低­从我的 Windows 8 本书的演示程序演示。 在 DirectWrite,你可以使用 IDWriteTextLayout 的 SetTypography 方法与一个 IDWriteTypography 对象,最终引用广泛的 DWRITE_FONT_FEATURE_TAG 枚举的成员。 但这些技术不是很像某些作为精确地指定标志符号。

斜体和粗体与 DrawGlyphRun 如何? 在许多情况下,不同的字体文件将包含斜体和粗体变化的一种字体。 在 GlyphDump 程序所包含的字体是加粗字体和光的字体的几个。 然而,您还可以选择以模拟斜和加粗字符在 CreateFontFace 中使用 DWRITE_FONT_SIMULATIONS 标志。

进展和偏移量

在 GlyphDump 和 FancyTitle 的两个字段中设置 DWRITE_GLYPH_RUN 为 nullptr。 这两个字段被命名为 glyphAdvances 和 glyphOffsets。

当您显示数组的标志符号时,您指定的第一个字符左基线的起源。 为每个连续的字符的水平坐标的原点是自动增加基于字符的宽度。 (类似的过程发生时显示横向文本)。这种增加是称为"前进"。

您可以获得 DirectX 为间距字符使用通过调用 IDWriteFontFace 的 GetDesignGlyphMetrics 方法的进展。 结果是一个 DWRITE_GLYPH_METRICS 结构,一个为您感兴趣的每个标志符号索引数组。 这一结构的 advanceWidth 字段指示与 designUnitsPerEm 领域的 GetMetrics 方法的 IDWriteFontFace,从获得的 DWRITE_FONT_METRICS 结构的进展和其中还包括适用于特定字体中的所有标志符号的垂直量度。

或者,您可以调用 GetDesignGlyphAdvances 方法的 IDWriteFontFace1,还提供了相对于 designUnitsPerEm 值的进步。 值除以 designUnitsPerEm (这通常是一个不错的倒圆角值,如 2,048),然后乘以在 DWRITE_GLYPH_RUN 中指定的 em 大小。

GlyphAdvances 数组常用空间更接近的字符在一起或分开进一步比设计指标显示。 如果您决定使用它,你需要将 glyphAdvances 设置为至少是的标志符号索引,减去一数组的大小值的数组。 提前不需要上的最后的标志符号,因为之外,则不显示。

GlyphOffsets 字段是一个 DWRITE_GLYPH_OFFSET 结构,另一个用于每个字符数组。 这两个字段是 advanceOffset 和 ascenderOffset,指示所需的偏移量 (向右,分别和向上) 相对于字符的正常位置。 这种设施在 XPS 文件通常用于整页都指定特定字体的多个标志符号的位置。

CenteredGlyphDump 程序演示如何使用 glyphOffsets 字段来显示整个数组的字形从单个 DrawGlyphRun 调用的特定字体文件:

context->DrawGlyphRun(Point2F(), &m_glyphRun, m_blackBrush.Get());

传递给 DrawGlyphRun 的坐标是 (0,0),和 glyphAdvances 设置为零值的数组,以抑制推进连续标志符号。 每个标志符号的位置完全受 glyphOffsets。 这一立场基于字形度量标准来中心每个标志符号在其列。 结果如图 4 所示。

The CenteredGlyphDump Program
图 4 CenteredGlyphDump 程序

提供您自己的字体加载程序

如果一个字体文件是一个应用程序包的一部分,它是相当容易,从中你可以使用 CreateFontReference 的文件路径。 但如果您有坐落在 XPS 或 EPUB 包中的字体或也许在独立存储中或在云计算吗?

只要您可以编写代码来访问字体文件,DirectX 可以访问它。 您需要提供两个类,一个实现 IDWriteFontFileLoader,另一种实现 IDWriteFontFileStream。 一般情况下,实现 IDWriteFontFileLoader 的类是单身人士,访问您的应用程序将需要,并将他们每个人都分配一个键的所有字体文件。 您的 IDWriteFontFileLoader 实现中的 CreateStreamFromKey 方法返回的每个字体文件 IDWriteFontStream 的实例。

若要使用这两个类,第一次实例化的实现 IDWriteFontFileLoader 的类的单个实例中,并将它传递给你的 IDWriteFactory 对象的 RegisterFontFileLoader。 然后,而不是调用 CreateFontFileReference 获取 IDWriteFontFile 的对象,调用 CreateCustomFontFileReference 与此 IDWriteFontFileLoader 实例和一个密钥识别所需的特定的字体文件。

CenteredGlyphDump 程序中演示了这种技术。 该项目包括两个类 — — PrivateFontFileLoader 和 PrivateFontFileStream — — 实现两个接口。 类访问字体文件在应用程序包中,但他们可能会用于其他目的的适应。

很可能您的 IDWriteFontFileLoader 实现将需要进行文件的 I/O 调用,并在 Windows 存储应用程序中,这些调用必须是异步的。 它使得有很多意义为 IDWriteFontFileLoader 类定义的所有字体都加载到内存中,异步方法和 IDWriteFontFileStream 简单地返回到这些内存块的指针。 这是我带 PrivateFontFileLoader 和 PrivateFontFileStream 后仔细审查微软示例代码之间的 Direct2D 杂志 App 示例 Windows 8.1, 的办法。

对于键来标识每个文件,我用文件名。 PrivateFontFileLoader 中的异步加载方法完成后,CenteredGlyphDump 程序为列表框中获取这些文件名。 这就是为什么只有文件名显示在图 4。 该程序是无知的每个文件关联的字体系列名称。

从到的标志符号的字符

当然,如果通过引用字形显示文本则很好你知道到底什么你需要的字形但您可以显示正常的 Unicode 字符,使用 DrawGlyphRun 吗?

可以,因为 IDWriteFontFace 有一个 GetGlyphIndices 方法,将字符代码转换为特定的字体文件的标志符号索引。 这样做,它会接收默认字形为每个字符的代码,所以你不会得到任何花哨的替代品。

但您将传递给 GetGlyphIndices 的字符代码中的所有窗口也许是独一无二的窗体中。 而不是 16 位字符代码 (如你通常工作与 Unicode 字符的字符串的时候是这种情况),您需要创建一个数组的 32 位字符代码。 如你所知,Unicode 实际上 21 位字符集,但字符通常存储为 16 位值 (称为 UTF-16),这意味着一些字符组成两个 16 位值。 但 GetGlyphIndices 想要 32 位值。 除非您的字符字符串有个字符与 0xFFFF 上面的代码,你可以简单地传送的值从一个数组到另一个。

如果你在处理正常字符从拉丁字母,您还可以假定是有一对一的对应关系之间的字符和字形。 否则,您可能需要创建一个更大的输出数组来接收指数的。

HelloGlyphRun 项目,足以证明这一简单技术。 图 5 显示加载的字体文件并设置一个 DWRITE_GLYPH_RUN 结构的代码。 更新方法调整标志符号偏移量以实现一种涟漪效应的文本。

图 5 定义 DWRITE_GLYPH_RUN 从一个字符的字符串

// Convert string to glyph indices
std::wstring str = L"Hello, Glyph Run!";
uint32 glyphCount = str.length();
std::vector<uint32> str32(glyphCount);
for (uint32 i = 0; i < glyphCount; i++)
     str32[i] = str[i];
m_glyphIndices = std::vector<uint16>(glyphCount);
m_fontFace->GetGlyphIndices(str32.data(), glyphCount, m_glyphIndices.data());
// Allocate array for offsets (set during Update)
m_glyphOffsets = std::vector<DWRITE_GLYPH_OFFSET>(glyphCount);
// Get output bounds
Windows::Foundation::Size outputBounds = m_deviceResources->GetOutputBounds();
// Define fields of DWRITE_GLYPH_RUN structure
m_glyphRun.fontFace = m_fontFace.Get();
m_glyphRun.fontEmSize = outputBounds.Width / 8; // Empirical
m_glyphRun.glyphCount = glyphCount;
m_glyphRun.glyphIndices = m_glyphIndices.data();
m_glyphRun.glyphAdvances = nullptr;
m_glyphRun.glyphOffsets = m_glyphOffsets.data();
m_glyphRun.isSideways = false;
m_glyphRun.bidiLevel = 0;

虽然您很可能将使用 DrawGlyphRun 只与你熟悉的字体文件,它是可能确定在运行时特定的字体文件中包含什么类型的标志符号。 IDWriteFontFace 定义访问表中的 OpenFont 文件的 TryGetFontTable 方法。 使用的"cmap"标记为字符到标志符号表和"GSUB"为标志符号替换表,但要准备要成功读取这些表的 OpenType 规范许多难以忍受小时。

标志符号运行使用系统字体

只是 DrawGlyphRun 为您提供自己的字体文件好吗? 起初似乎是这样,但您可以使用它,以及系统字体。 这里是的过程:使用 IDWriteFactory 的 GetSystemFontCollection 方法获取一个 IDWriteFontCollection 对象。 该对象允许您查找在系统上安装的字体相关联的所有家庭名称。 IDWriteFontCollection 的 GetFontFamily 方法返回一个 IDWriteFontFamily 类型的对象。 从那,你可以打电话 GetMatchingFonts 或 GetFirstMatchingFont,字体系列结合斜体、 粗体和拉伸属性以获得 IDWriteFont

一旦您有一个 IDWriteFont 对象,调用 CreateFontFace 来获取 IDWriteFontFace 对象。 这是对象的同一类型在早期版本的程序从 CreateFontFace 获得 ! 从该对象,就可以开始为 DrawGlyphRun DWRITE_GLYPH_RUN 结构设置。

这表现在 SystemFontGlyphs 项目中。 该项目使用在 GetSystemFontCollection 在其 DirectXPage 类中填充与系统字体系列名称的列表框。 当选定项,则 IDWriteFontFace 被派生的对象传递到渲染器。

SystemFontGlyphsRenderer 类生成一个 DWRITE_GLYPH_RUN 结构基于文本"恼人振动文本效果"。Update 方法然后不得不将标志符号的偏移量数组的值设置为-3 和 3,之间的随机数字,使它看起来,如果文本字符串中的所有字符都独立地都振动。

那里似乎不是很多的 IDWriteFont 和 IDWriteFontFace 之间的概念区别除了,IDWriteFont 始终表示的字体,不是 IDWriteFontFace 需要时 (如系统字体集合) 的字体集合的一部分。 IDWriteFontCollection 具有一个 GetFontFromFontFace 方法,接受 IDWriteFontFace,并返回相应的 IDWriteFont,但它只有在与 IDWriteFontFace 关联的字体文件是字体集合中的一部分。

自定义字体集合

现在你已经看到如何使用 DrawGlyphRun 将与您的应用程序加载的字体文件和系统字体。 它是可能要使用常规文本输出方法 (DrawText 和 DrawTextLayout) 与您自己的字体文件吗?

是的,确实如此。 你会记得 DrawText 和 DrawTextLayout 需要一个 IDWriteTextFormat 对象。 您创建此使用 IDWriteFactory 的 CreateTextFormat 方法,通过指定一个字体系列名称和字体集合。

通常你的字体集合参数设置为 nullptr 以指示系统字体。 但你也可以通过调用 IDWriteFactory 的 CreateCustomFontCollection 方法来创建一个自定义字体集合。 您需要提供您自己的类实现 IDWriteFontCollectionLoader 接口的和另一个实现的类,IDWriteFontFileEnumerator。

ResourceFontLayout 程序演示了这一点。 该程序还包括接口的实现的 IDWriteFontFileLoader 和 IDWriteFontFileStream 从居中­GlyphDump。 当您从该自定义字体集合获取字体系列的列表时,你会发现多个字体文件有时被合并为一个单一的字体系列。

在框的底部奖

此时,IDWriteFontFace 的重要性应该是相当明显的。 您可以创建一个 IDWriteFontFace 对象从两个方向:或者直接从一个字体文件,或者通过选择特定字体的字体集合。 你然后引用此 IDWriteFontFace 在 DWRITE_GLYPH_RUN 结构中,或使用它来获取字形度量或访问字体文件中的表。

IDWriteFontFace 还定义了一个名为 GetGlyphRun 方法­大纲。 此方法的参数是相同的字段的 DWRITE_GLYPH_RUN 结构,但一个额外的参数是 IDWriteGeometrySink,这是 ID2D1SimplifiedGeometrySink 相同。

这意味着您可以将一个文本字符串转换为 ID2D1PathGeometry,然后呈现和操作那几何在任何你想要的方式。 图 6 是从 OutlinedCharacters 程序,可显示字符几何已描边和填充 ; 截图 用一个虚线样式 (这动画效果,使周游字符的点) ; 呈现相同的几何 和几何扩阔和概述,实际上,其中概述了轮廓。

The OutlinedCharacters Program
图 6 OutlinedCharacters 程序

我才只是开始。

Charles Petzold 是 MSDN 杂志和作者的"编程窗口,第 6 版"长期贡献 (O'Reilly 媒体,2012年),一本关于编写应用程序的 Windows 8 书。 他的网站是 charlespetzold.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:Jim Galasyn 和贾斯汀 Panian