中文字体的FontMetrics解析
因行业对字体大小要求严格参考相关规范,因此对通过渲染引擎绘制的文本字体把控严格。
而在Skia/openGL/Qt等主流渲染引擎中,所有设置字体大小(FontSize),都是以英文字体大小作为标准的,准确来说,对应的应该是FontMetrics中的CapHeight作为英文字体高度。
笔者发现在网络上相关资料中,很少有关于中文字符的FontMetrics解析。通过多日研究后,在此详细解析一下中文字体的FontMetrics。
一、FontMetrics标准解析
研究发现,基本所有的渲染引擎都采用以下的字体绘制结构,以下简称标准
这里各个属性都已经描述的很清楚了,网络上其余各个版本的,都不太正确。
为了对比,我使用Skia渲染引擎绘制了同样的文字来做对比,效果如下:
实际上实测的效果还是与网上流传的示意图有一定差异的。
1、大部分字体,字符的顶部触碰不到AscentLine。AscentLine用于带上标的字符、拉丁文字符等
2、对于输入的字符不同,计算的Top、Bottom位置并非不变的。(例如斜体单个字符h的Top、Bottom就要比非斜体jEh要窄些)
大体上都参考标准,研究得出几条结论:
1、英文正常高度为Baseline到CapHeight线
2、最低位置到Descent线处
3、带上标的字符最高到Ascent线处
4、斜体并不改变字体高度
二、中文字符的FontMetrics
绘制引擎就是按照英文字符构造来设计的几条线,压根没考虑中文字符的感受。
假设设置字体高度为100像素,绘制出来的英文字体为100个像素,对于中文来说,往往是会放大。所有的字体的中文的字符都不会在CapHeight内
以宋体和等线体为例:
注:与上图英文字符的绘制线一样,绿色的Ascent与Descent和Top与Bottom几条线重合了,因此绿色看不出来是绿色了。
可以清楚的看到,无论哪种字体,中文字符都不在CapHeight和BaseLine之间。而且并不与任一条线接触。
理论上来说,我们需要将中文字符的高度改成我们设置的高度,就需要缩放。
公式为:
因此,想要知道需要设置的字体高度,公式为:
其中,中文字符应有高度为100像素,当前设置字体高度为100像素,所以只需要量算出中文字符当前高度,就能计算出这个应设置的字体高度。
但是目测量算的都不准确,应该使用FontMetrics中有的属性来确定真实值。
经过多测量算、推演,得出以下规律:
1、以宋体、仿宋体等为代表的标准中文字体类型:
上下两条紫色线为中文字符上下限高度
中文字符下限为UnderLinePos,上限为 Capheight + DescentLine
是的没有看错,上线是DescentLine(即BaseLine到DescentLine的距离),而不是AscentLine。
实际上AscentLine与Top重合了,是要比DescentLine要大一点
至于为什么是DescentLine实际上我也不明白,但能肯定的是这不是偶然。无论如何缩放这个值始终是能和中文字符上线重合,一定存在规律。
2、以等线体为代表的的中英文字体类型:
这类字体非常奇怪,首先英文字符的下限并没有到Descent,甚至没有到UnderLine,与纯英文字体如Times NewRoman等规律就不同。
中文字符也是不能符合上面宋体的规律。
但经过多次推算(不同尺度下),发现一个规律:
中文字符高度 = CapHeight + UnderLinePos
能得出这个结论我也是比较疑惑的,没有任何API、文献资料能证明这条规律,但事实上它就是如此。
得出中文字符高度后,再带入公式,求出应该设置的FontSize,再设置新FontSize,绘制出来的中文字符,就是我们想要的字体高度了。
至此,中文字符高度的计算工作完毕。
期间使用到的代码:
1.绘制字体基准线代码
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(3);
paint.setColor(0xffff0000);//红色为fTop\fBottom\BaseLine
double y = p.y();
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
y = p.y() - font_m.fTop;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
y = p.y() - font_m.fBottom;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
paint.setStrokeWidth(0);
paint.setColor(0xff00ff00);//绿色为Ascent\Descent
y = p.y() - font_m.fAscent;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
y = p.y() - font_m.fDescent;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
paint.setColor(0xffffff00);//黄色为fCapHeight
y = p.y() + font_m.fCapHeight;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
paint.setColor(0xffff00ff);//紫色为fUnderlinePosition、宋体、仿宋体等中文字符顶部
y = p.y() - font_m.fUnderlinePosition;
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
y = p.y() + font_m.fCapHeight + std::fabs(font_m.fDescent);
canvas->drawLine(p.x(), y, p.x() + length, y, paint);
//paint.setColor(0xffffffff);//白色为fStrikeoutPosition
//y = p.y() + font_m.fCapHeight + font_m.fDescent;
//canvas->drawLine(p.x(), y, p.x() + length, y, paint);
2.判断是否中文类字体(目前只用字体名判断,希望能有其他方法)
bool _isSpecialZhFamily(const dan::SGString& fontName)
{
if(fontName == "宋体" || fontName == "仿宋")
{
return true;
}
return false;
}
3.按照中文标准高度换算应设置的字体高度
auto text_height = std::fabs(font_m.fCapHeight);//英文标高,所有字体的英文标高都是CapHeight
if(this->isFontSizeOnZhChar()) //中标高为准
{
//对于部分中文字体,需要用其他修正方式
text_height = std::fabs(font_m.fCapHeight) + std::fabs(font_m.fUnderlinePosition);
if(_isSpecialZhFamily(_paint->font().family()))
{
text_height += std::fabs(font_m.fDescent);
}
length /= TEXT_LENGTH_RESIZE_CONST;//长度修正,中文标高不需要修正
}
double scale = font_size / text_height;
auto scaled_fontsize = font_size * scale;//缩放后的文本大小
skFont.setSize(scaled_fontsize);