一个字体文件只能支持有限的字符数量,为了能够知道某个字体包含哪些字符,可通过 DirectWrite 提供的 GetUnicodeRanges 方法获取。本文将演示如何从 IDWriteFontFace 的 GetUnicodeRanges 方法获取字体能支持的字符范围以及对比 WPF 的行为

需要通过以下路径才能获取到 IDWriteFontFace3 实例

  • 通过 DWrite 的 DWriteCreateFactory 创建 IDWriteFactory 工厂
  • 通过 IDWriteFactory 工厂的 CreateFontFaceReference 方法,从字体文件获取到 IDWriteFontFaceReference 对象
  • 从 IDWriteFontFaceReference 的 CreateFontFace 方法获取到 IDWriteFontFace3 实例

拿到 IDWriteFontFace3 实例之后,即可调用 GetUnicodeRanges 方法获取字体能支持的字符范围数组

以下示例代码将尝试获取宋体字体能够支持的字符范围

先创建 IDWriteFactory 工厂,代码如下

        DWrite dWrite = DWrite.GetApi();
        ComPtr<IDWriteFactory6> factory = dWrite.DWriteCreateFactory<IDWriteFactory6>(FactoryType.Shared);

从字体文件获取 IDWriteFontFaceReference 对象,代码如下

    private const string FontFile = @"C:\windows\fonts\simsun.ttc";

        // 宋体字体
        var fontFile = FontFile;

            IDWriteFontFaceReference* fontFaceReference;

            fixed (char* pFontFile = fontFile)
            {
                hr = factory.Handle->CreateFontFaceReference(pFontFile, null, (uint) 0, FontSimulations.None,
                    &fontFaceReference);
                hr.Throw();
            }

从 IDWriteFontFaceReference 获取 IDWriteFontFace3 实例,代码如下

            IDWriteFontFace3* fontFace3;
            fontFaceReference->CreateFontFace(&fontFace3);

调用 IDWriteFontFace3 的 GetUnicodeRanges 获取字符范围时,需要调用 GetUnicodeRanges 两次,第一次调用是获取数组长度,第二次才是获取字符范围,代码如下

            uint rangeCount = 0;
            fontFace3->GetUnicodeRanges(0, null, ref rangeCount);
            var unicodeRanges = new UnicodeRange[rangeCount];

            fixed (UnicodeRange* p = unicodeRanges)
            {
                fontFace3->GetUnicodeRanges(rangeCount, p, ref rangeCount);
            }

尝试拿到的 UnicodeRange 数组就包含了字体文件能够支持的字符范围了

获取之后,尝试输出到控制台,输出代码如下

            fixed (UnicodeRange* p = unicodeRanges)
            {
                fontFace3->GetUnicodeRanges(rangeCount, p, ref rangeCount);
            }

            for (var i = 0; i < unicodeRanges.Length; i++)
            {
                var unicodeRange = unicodeRanges[i];
                var start = new Rune(unicodeRange.First);
                var end = new Rune(unicodeRange.Last);

                Console.WriteLine($"Range {i}: '{start.ToString()}'({start.Value}) - '{end.ToString()}'({end.Value}) Length={end.Value - start.Value + 1}");
            }

尝试运行项目,可见控制台的输出大概如下

Range 0: ' '(32) - ''(127) Length=96
Range 1: '?'(160) - '?'(255) Length=96
Range 2: 'ā'(257) - 'ā'(257) Length=1
...
Range 130: '?'(13312) - '?'(19903) Length=6592
Range 131: '一'(19968) - '?'(40959) Length=20992
...
Range 156: '!'(65281) - '~'(65374) Length=94
Range 157: '¢'(65504) - '¥'(65509) Length=6

相比之下,在 WPF 框架内,获取字体能够支持的字符范围就简单多了。只需用 GlyphTypeface 的 CharacterToGlyphMap 获取即可。对比的测试逻辑如下

            uint rangeCount = 0;
            fontFace3->GetUnicodeRanges(0, null, ref rangeCount);
            var unicodeRanges = new UnicodeRange[rangeCount];
            fixed (UnicodeRange* p = unicodeRanges)
            {
                fontFace3->GetUnicodeRanges(rangeCount, p, ref rangeCount);
            }

            TestWpf(unicodeRanges);

    private void TestWpf(IReadOnlyList<UnicodeRange> unicodeRanges)
    {
        var wpfFontFamily = new FontFamily("宋体");
        Typeface typeface = wpfFontFamily.GetTypefaces().First();
        if (typeface.TryGetGlyphTypeface(out var glyphTypeface))
        {
            for (uint i = 0; i < 6000; i++)
            {
                if (IsInUnicodeRange(i))
                {
                    if (glyphTypeface.CharacterToGlyphMap.TryGetValue((int) i, out var glyphIndex))
                    {
                        // 在范围内的字符,可以找到对应的字形索引
                        _ = glyphIndex;
                    }
                    else
                    {
                        // 在范围内的字符,找不到对应的字形索引
                        Console.WriteLine($"Character {i} is not in the glyph map.");
                        Debugger.Break();
                    }
                }
                else
                {
                    // 不在范围内的字符,预期找不到对应的字形索引
                    if (glyphTypeface.CharacterToGlyphMap.TryGetValue((int) i, out var glyphIndex))
                    {
                        // 不在范围内的字符,居然可以找到对应的字形索引
                        _ = glyphIndex;
                        Debugger.Break();
                    }
                    else
                    {
                        // 不在范围内的字符,预期找不到对应的字形索引
                    }
                }
            }

            bool IsInUnicodeRange(uint codepoint)
            {
                foreach (var unicodeRange in unicodeRanges)
                {
                    if (codepoint >= unicodeRange.First && codepoint <= unicodeRange.Last)
                    {
                        return true;
                    }
                }

                return false;
            }
        }
    }

尝试运行以上代码,可以看到所有能够从 UnicodeRange 数组里面找到的字符,同样也能从 WPF 的 CharacterToGlyphMap 找到。所有在 UnicodeRange 数组范围之外的,也都不能在 CharacterToGlyphMap 找到。可以认为 WPF 的 CharacterToGlyphMap 的行为就和 GetUnicodeRanges 相同

核心代码如下


    // 宋体字体
    private const string FontFile = @"C:\windows\fonts\simsun.ttc";

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        DWrite dWrite = DWrite.GetApi();
        ComPtr<IDWriteFactory6> factory = dWrite.DWriteCreateFactory<IDWriteFactory6>(FactoryType.Shared);

        // 宋体字体
        var fontFile = FontFile;

        unsafe
        {
            HResult hr = 0;

            IDWriteFontFaceReference* fontFaceReference;

            fixed (char* pFontFile = fontFile)
            {
                hr = factory.Handle->CreateFontFaceReference(pFontFile, null, (uint) 0, FontSimulations.None,
                    &fontFaceReference);
                hr.Throw();
            }

            IDWriteFontFace3* fontFace3;
            fontFaceReference->CreateFontFace(&fontFace3);

            uint rangeCount = 0;
            fontFace3->GetUnicodeRanges(0, null, ref rangeCount);
            var unicodeRanges = new UnicodeRange[rangeCount];

            fixed (UnicodeRange* p = unicodeRanges)
            {
                fontFace3->GetUnicodeRanges(rangeCount, p, ref rangeCount);
            }

            for (var i = 0; i < unicodeRanges.Length; i++)
            {
                var unicodeRange = unicodeRanges[i];
                var start = new Rune(unicodeRange.First);
                var end = new Rune(unicodeRange.Last);

                Console.WriteLine($"Range {i}: '{start.ToString()}'({start.Value}) - '{end.ToString()}'({end.Value}) Length={end.Value - start.Value + 1}");
            }

            TestWpf(unicodeRanges);
        }
    }

    private void TestWpf(IReadOnlyList<UnicodeRange> unicodeRanges)
    {
        var wpfFontFamily = new FontFamily("宋体");
        Typeface typeface = wpfFontFamily.GetTypefaces().First();
        if (typeface.TryGetGlyphTypeface(out var glyphTypeface))
        {
            for (uint i = 0; i < 6000; i++)
            {
                if (IsInUnicodeRange(i))
                {
                    if (glyphTypeface.CharacterToGlyphMap.TryGetValue((int) i, out var glyphIndex))
                    {
                        // 在范围内的字符,可以找到对应的字形索引
                        _ = glyphIndex;
                    }
                    else
                    {
                        // 在范围内的字符,找不到对应的字形索引
                        Console.WriteLine($"Character {i} is not in the glyph map.");
                        Debugger.Break();
                    }
                }
                else
                {
                    // 不在范围内的字符,预期找不到对应的字形索引
                    if (glyphTypeface.CharacterToGlyphMap.TryGetValue((int) i, out var glyphIndex))
                    {
                        // 不在范围内的字符,居然可以找到对应的字形索引
                        _ = glyphIndex;
                        Debugger.Break();
                    }
                    else
                    {
                        // 不在范围内的字符,预期找不到对应的字形索引
                    }
                }
            }

            bool IsInUnicodeRange(uint codepoint)
            {
                foreach (var unicodeRange in unicodeRanges)
                {
                    if (codepoint >= unicodeRange.First && codepoint <= unicodeRange.Last)
                    {
                        return true;
                    }
                }

                return false;
            }
        }
    }

如果大家对 WPF 的 CharacterToGlyphMap 实现逻辑感兴趣,请参阅 读 WPF 源代码 了解获取 GlyphTypeface 的 CharacterToGlyphMap 的数量耗时原因

本文代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin e725768ffd131bda55560242748f4f26400ef006

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin e725768ffd131bda55560242748f4f26400ef006

获取代码之后,进入 DirectX/DWrite/JallwirekebalaChelchelkonuya 文件夹,即可获取到源代码

更多技术博客,请参阅 博客导航

参考文档:

IDWriteFontFace1::GetUnicodeRanges (dwrite_1.h) - Win32 apps - Microsoft Learn


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/DirectWrite-%E9%80%9A%E8%BF%87-GetUnicodeRanges-%E8%8E%B7%E5%8F%96%E5%AD%97%E4%BD%93%E8%83%BD%E6%94%AF%E6%8C%81%E7%9A%84%E5%AD%97%E7%AC%A6%E8%8C%83%E5%9B%B4.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利