本文将对 WPF 进行 GlyphTypeface 的 Baseline 行为测试。经过测试发现行为非常符合预期,这个值乘以字号就是基线

本文内容里面只给出关键代码片段,如需要全部的项目文件,可到本文末尾找到本文所有代码的下载方法

前置博客: WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本

大飞哥来问我关于文本行距和基线问题,在之前某只不远透露姓名的牛写了一段有趣的代码,设定了行距计算里面包含 1/5 的魔法数字。我开始猜测是基线计算的问题,结果一顿计算发现数据差异过大,没有解决开始的问题,只好将我测试的 GlyphTypeface 的 Baseline 行为记录

在 WPF 里面,可以通过 FontFamily 根据字体名字符串获取到 GlyphTypeface 对象,大概的代码如下

        var fontFamily = new FontFamily("微软雅黑");

        Typeface typeface = fontFamily.GetTypefaces().First();

        var success = typeface.TryGetGlyphTypeface(out GlyphTypeface glyphTypeface);
        if (!success)
        {
            Debug.Fail("微软雅黑字体找不到");
        }

我尝试绘制一段文本,内容是“文本测试afgjqiWHXx”

这段文本的特征是中英文混排,且英文字符有穿越基线的字符

我尝试按照 WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本 博客提供的方法构建 GlyphRun 进行绘制,代码如下

        var fontSize = 30;

        var text = "文本测试afgjqiWHXx";
        var glyphIndexList = new List<GlyphInfo>();

        for (var i = 0; i < text.Length; i++)
        {
            var codePoint = (int) text[i]; // 这里的 Code Point 没有处理 Emoji 的高低代理字符
            if (glyphTypeface.CharacterToGlyphMap.TryGetValue(codePoint, out var glyphIndex))
            {
                var width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;
                var height = glyphTypeface.AdvanceHeights[glyphIndex] * fontSize;
                glyphIndexList.Add(new GlyphInfo(glyphIndex, width, height));
            }
            else
            {
                // 进入字体回滚
            }
        }

        var pixelsPerDip = (float) VisualTreeHelper.GetDpi(this).PixelsPerDip;

        var baseline = glyphTypeface.Baseline * fontSize;

        var location = new Point(0, baseline);
        drawingContext.PushGuidelineSet(new GuidelineSet([0], [baseline]));

        var defaultXmlLanguage =
            XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.IetfLanguageTag);

        var glyphRun = new GlyphRun
        (
            glyphTypeface,
            bidiLevel: 0,
            isSideways: false,
            renderingEmSize: fontSize,
            pixelsPerDip: pixelsPerDip,
            glyphIndices: glyphIndexList.Select(t => t.GlyphIndex).ToList(),
            baselineOrigin: location, // 设置文本的偏移量
            advanceWidths: glyphIndexList.Select(t => t.AdvanceWidth).ToList(), // 设置每个字符的字宽,也就是字号
            glyphOffsets: null, // 设置每个字符的偏移量,可以为空
            characters: text.ToCharArray(),
            deviceFontName: null,
            clusterMap: null,
            caretStops: null,
            language: defaultXmlLanguage
        );

        drawingContext.DrawGlyphRun(Brushes.Black, glyphRun);

我尝试使用 DrawLine 将 baseline 的值绘制出来,代码如下

        drawingContext.DrawLine(new Pen(Brushes.Black,1), new Point(0, baseline), new Point(300, baseline));

运行代码,可见画出来的线条就刚好是文本的基线,非常正确

如此可证明将 GlyphTypeface 的 Baseline 属性乘以字号就是文本字符的基线

那 GlyphTypeface 的 Baseline 属性和 FontFamily 的有什么不同?绝大部分字体这两个属性都是相同的,但是由于字体可能存在加粗斜体等,为了更好的视觉呈现,确实存在不同的情况。有些 GlyphTypeface 和 FontFamily 存在不相同的 Baseline 属性

对于最终渲染来说,就应该获取对应的 GlyphTypeface 的基线。但由于 GlyphTypeface 和 FontFamily 的基线基本相差不大,也可以放心直接就用 FontFamily 的基线就好。毕竟在很多文本排版里面,是不期望只是加粗或带斜体一下,就让字体在行内上浮下沉

对于一些字体设计师来说,会特别修改加粗的基线,虽然从排版数值上让字体下沉,但视觉效果却刚好看起来是顺着的。从这个思路上说,拿 GlyphTypeface 的基线是更加正确的

通过 FormattedText 获取到的 Baseline 基本等于 FontFamily 的 Baseline 乘以字号,可能会和 GlyphTypeface 的不相同,如以下代码片段

                    var text = "1";
                    var fontSize = 30;

                    var formattedText = new FormattedText(text, CultureInfo.CurrentCulture,
                        System.Windows.FlowDirection.LeftToRight, typeface, fontSize, Brushes.Black, pixelsPerDip);
                    var sameGlyphTypefaceAndFormattedText = Math.Abs(formattedText.Baseline - glyphTypeface.Baseline * fontSize) < 0.01; 
                    var sameFontFamilyAndFormattedText = Math.Abs(formattedText.Baseline - fontFamily.Baseline * fontSize) < 0.01;

在我设备上的所有字体都是 sameFontFamilyAndFormattedText 为 true 的值。即如果只是想通过 FormattedText 获取基线,那完全和使用 FontFamily 的 Baseline 乘以字号是等价的

通过阅读 WPF 源代码,可以理解到 FormattedText 的 Baseline 为什么和 FontFamily 几乎等价,原因是 FormattedText 的 Baseline 是从首行 TextLine 的 Baseline 获取到的。在 SimpleTextLine 类型里面的 Baseline 属性定义如下

SimpleRun run = (SimpleRun)runs[count];
var realAscent = Math.Max(realAscent, run.Baseline);
_baselineOffset = formatter.IdealToReal(TextFormatterImp.RealToIdeal(realAscent), PixelsPerDip);

        /// <summary>
        /// Client to get the distance from top to baseline of this text line
        /// </summary>
        public override double Baseline
        {
            get { return _baselineOffset; }
        }

而 SimpleRun 的 Baseline 定义如下

        internal double Baseline
        {
            get
            {
                if (Ghost || EOT)
                    return 0;

                return TextRun.Properties.Typeface.Baseline(TextRun.Properties.FontRenderingEmSize, 1, _pixelsPerDip, _textFormatterImp.TextFormattingMode);
            }
        }

可见是进入到 Typeface 的 Baseline 方法里面

    public class Typeface
    {
        internal double Baseline(double emSize, double toReal, double pixelsPerDip, TextFormattingMode textFormattingMode)
        {
            return CachedTypeface.FirstFontFamily.Baseline(emSize, toReal, pixelsPerDip, textFormattingMode);            
        }
    }

如此可以看到,绕了一圈还是回到了 IFontFamily 的 Baseline 方法。来对比一下 FontFamily 类型的 Baseline 属性,以及 IFontFamily 接口的 PhysicalFontFamily 实现的 Baseline 方法

    public class FontFamily
    {
        internal IFontFamily FirstFontFamily { get; }

        public double Baseline
        {
            get
            {
                return FirstFontFamily.BaselineDesign;
            }

            set
            {
                VerifyMutable().SetBaseline(value);
            }
        }
    }

    internal sealed class PhysicalFontFamily : IFontFamily
    {
        double IFontFamily.BaselineDesign
        {
            get
            {
                return ((IFontFamily)this).Baseline(1, 1, 1, TextFormattingMode.Ideal);
            }
        }

        double IFontFamily.Baseline(double emSize, double toReal, double pixelsPerDip, TextFormattingMode textFormattingMode)
        {
            if (textFormattingMode == TextFormattingMode.Ideal)
            {
                return emSize * _family.Metrics.Baseline;
            }
            else
            {
                double realEmSize = emSize * toReal;
                return TextFormatterImp.RoundDipForDisplayMode(_family.DisplayMetrics((float)(realEmSize), checked((float)pixelsPerDip)).Baseline * realEmSize, pixelsPerDip) / toReal;
            }
        }
    }

如此可见 FormattedText 走的逻辑和 FontFamily 基本相同,只有一些数值上的差异而已

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

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

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

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

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

获取代码之后,进入 WPFDemo/LahallgucheHichawaki 文件夹,即可获取到源代码

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

附录:

以下是我使用如下代码跑出来的基线集合

        var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;

        foreach (FontFamily? fontFamily in System.Windows.Media.Fonts.SystemFontFamilies)
        {
            if (!fontFamily.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("zh-CN"),out var name))
            {
                name = fontFamily.Source;
            }
            foreach (var typeface in fontFamily.GetTypefaces())
            {
                var typefaceName = typeface.FaceNames.First().Value;
                if (typeface.TryGetGlyphTypeface(out GlyphTypeface? glyphTypeface))
                {
                    var text = "1";
                    var fontSize = 30;

                    var formattedText = new FormattedText(text, CultureInfo.CurrentCulture,
                        System.Windows.FlowDirection.LeftToRight, typeface, fontSize, Brushes.Black, pixelsPerDip);
                    var sameGlyphTypefaceAndFormattedText = Math.Abs(formattedText.Baseline - glyphTypeface.Baseline * fontSize) < 0.01; 
                    var sameFontFamilyAndFormattedText = Math.Abs(formattedText.Baseline - fontFamily.Baseline * fontSize) < 0.01;

                    Debug.WriteLine($"""
                                     字体名: {name} - {typefaceName}
                                     斜体: {glyphTypeface.Style}
                                     加粗: {glyphTypeface.Weight}
                                     拉伸: {glyphTypeface.Stretch}
                                     基线 FontFamily {fontFamily.Baseline}
                                     基线 GlyphTypeface {glyphTypeface.Baseline}
                                     基线 FormattedText {formattedText.Baseline / fontSize}
                                     基线相同 FontFamily == GlyphTypeface {fontFamily.Baseline == glyphTypeface.Baseline}
                                     基线相近 GlyphTypeface ~ FormattedText {sameGlyphTypefaceAndFormattedText}
                                     基线相近 FontFamily ~ FormattedText {sameFontFamilyAndFormattedText}
                                     
                                     """);
                }
            }
        }

输出内容如下,也欢迎大家在自己的设备上运行以上代码

字体名: 更纱终端书呆黑体-简 - Regular
斜体: Normal
加粗: Normal
拉伸: Normal
基线 FontFamily: 0.965
基线 GlyphTypeface: 0.965
基线 FormattedText: 0.9650000000000001
基线相同 FontFamily == GlyphTypeface: True
基线相近 GlyphTypeface ~ FormattedText: True
基线相近 FontFamily ~ FormattedText: True

字体名: 汉仪南宫体简 - Regular
斜体: Normal
加粗: Normal
拉伸: Normal
基线 FontFamily: 0.998046875
基线 GlyphTypeface: 0.859375
基线 FormattedText: 0.998
基线相同 FontFamily == GlyphTypeface: False
基线相近 GlyphTypeface ~ FormattedText: False
基线相近 FontFamily ~ FormattedText: True

字体名: 汉仪南宫体简 - Oblique
斜体: Oblique
加粗: Normal
拉伸: Normal
基线 FontFamily: 0.998046875
基线 GlyphTypeface: 0.859375
基线 FormattedText: 0.998
基线相同 FontFamily == GlyphTypeface: False
基线相近 GlyphTypeface ~ FormattedText: False
基线相近 FontFamily ~ FormattedText: True

字体名: 汉仪南宫体简 - Bold
斜体: Normal
加粗: Bold
拉伸: Normal
基线 FontFamily: 0.998046875
基线 GlyphTypeface: 0.859375
基线 FormattedText: 0.998
基线相同 FontFamily == GlyphTypeface: False
基线相近 GlyphTypeface ~ FormattedText: False
基线相近 FontFamily ~ FormattedText: True

字体名: 方正硬笔行书简体_非压缩版 - Regular
斜体: Normal
加粗: Normal
拉伸: Normal
基线 FontFamily: 0.82421875
基线 GlyphTypeface: 0.76953125
基线 FormattedText: 0.8242222222222222
基线相同 FontFamily == GlyphTypeface: False
基线相近 GlyphTypeface ~ FormattedText: False
基线相近 FontFamily ~ FormattedText: True

...

本文会经常更新,请阅读原文: https://blog.lindexi.com/post/WPF-%E6%B5%8B%E8%AF%95-GlyphTypeface-%E7%9A%84-Baseline-%E8%A1%8C%E4%B8%BA.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

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

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

微软最具价值专家


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

以下是广告时间

推荐关注 Edi.Wang 的公众号

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

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