〇、前言
现如今,说起CSS布局方式,你可能想到的是Flex布局,或者是最新的Grid布局。不过,仍然有许多前端工程师使用行内布局来实现多列自适应、图片垂直居中等效果,更别说,在处理与文字排版相关的功能时,行内布局仍然是不二之选。
然而,在我使用行内布局的时候,常常会遇到vertical-align: middle
怎么也不居中,或者行内元素和容器之间有一些莫名其妙的空隙等问题。对于有些问题,即使能从网上找到解决方案,我也搞不清楚这么做的原因,例如:为什么加上font-size: 0
就好了(多数情况下,写这个样式的人并不是为了隐藏文字)。
于是后来,我系统性地学习了一下行内布局,阅读了大量CSS规范文档和技术博客,最后整理成这个系列文章,希望能帮助你真正深入到行内布局之中。随着文章的推进,你会发现行内布局其实是一个相当复杂的知识点,其中有诸多你不曾注意的盲区。
本文是系列的开篇和基础,将带你走进字体内部,向你解释:
- 现代字体设计的一些基本概念
font-size
到底是什么意思,它是如何作用于文字大小的?- 行内元素的内容区域的高度由什么决定?
- 为什么同一个行内元素,在不同操作系统或浏览器中的高度可能不同?
本文讨论的主题涉及到平台兼容性问题,特此说明,本文所有的描述和结论基于以下平台:
- MacOS 10.13:Chrome 104、Safari 13.1、Firefox Developer Edition 105
- Windows 7:Chrome 104
- iOS 15: Safari
一、引子
首先,还是让我们从一个简单的例子开始。
在这个例子中,我做了以下事情:
- 在CSS中引入Catamaran字体(感谢googlefonts.cn提供谷歌字体服务)
- 为HTML中唯一的<span>设置了
font-family
、font-size
和outline
- 等待Catamaran加载完成后,显示出<span>的高度
正常情况下,最后显示出来的<span>的高度应该是164px。那么问题来了:
- 你知道
font-size: 100px
里的100px表示的是哪一段距离吗?是A的高度吗? - 你知道这个<span>的高度为什么是164px吗?为什么不是100px?
为了寻找上面两个问题的答案,你可能首先想到的就是去查文档。那么,我们就来看看CSS规范里对这两个问题的解释。
首先是font-size
:
This property indicates the desired height of glyphs from the font. For scalable fonts, the font-size is a scale factor applied to the EM unit of the font. (此属性用于设置字体中字形的高度。对于可缩放字体,font-size是字体中EM unit的缩放因子。)CSS Fonts Module Level 4
然后是<span>的高度问题(由于在例子中并未设置padding
或border
之类的属性,所以<span>的高度就是内容区域的高度):
The content area of the inline box is sized and positioned to fit (possibly hypothetical) text from its first available font. This specification does not specify how. A UA may, e.g., use the maximum ascender and descender of the font. (行内框的内容区域的大小和位置由第一个可用字体决定。本规范没有具体说明如何决定。例如,UA可以使用字体的最大升部和降部。)CSS Inline Layout Module Level 3
看完CSS规范的解释,你可能依然觉得一脸懵逼。不过没关系,至少我们知道了这两个问题都和字体有关。那么接下来,就让我们进入字体内部,来看看CSS规范说的到底是什么意思。
二、走进字体
目前,比较常见的字体格式有OpenType(.otf, .ttf)、TrueType(.ttf)和Web Open Font Format(.woff, .woff2)。这几种格式虽有不同,但是就本文涉及的知识点,这三者的设计是一样的,故下文不作区分,且结论同时适用于三者。
以Catamaran字体为例,让我们用字体设计软件打开来看一下,我这里用的是FontLab。假设我们要修改A和x的字形,那么进入到字形设计界面之后,就可以看到类似下面这张图一样的界面。为了方便描述,我从原图中截取了最重要的部分,并绘制了坐标轴,加粗显示了参考线及其名称。
在图中,每个字形都置于一个直角坐标系中,且有5条参考线(需要强调的是,这些参考线不是我自己加的,而是所有字体都有这5个属性,只不过最终以参考线的形式显示在坐标系中而已):
- baseline:也就是基线,应该大家都比较熟悉。基线处于纵坐标0的位置,所以永远和横轴重合。
- ascent:字体中的所有字形从基线往上,最高不能超过的值,可以说是理论上的字形上边界,也就是CSS规范中提到的maximum ascender(ascender是指其他字形高于小写字母x的部分)。
- descent:字体中的所有字形从基线往下,最低不能超过的值,可以说是理论上的字形下边界,也就是CSS规范中提到的maximum descender(descender是指所有字形低于基线的部分)。
- x height:字体中小写字母x的高度。
- capital height:大写字母的高度。
所以我们可以从图中直观看出,A的高度为680,而x的高度为485。另外,由于这些字形都是矢量图形,所以这里的680和485等数字的单位不是像素、厘米或者磅,而是一个相对单位——unit。
那么,这些矢量的字形是怎么和font-size
对应起来的呢?这时候就需要用到字体中的另一个单位——em(读音与字母M相同),它是矢量字形与font-size
之间的一座桥梁(其实更像是现实中美元的作用)。
一方面,在字体中有一个属性叫做UPM(Units Per EM)。顾名思义,UPM指定多少个unit组成一个em,这个值一般是1000或者2048。例如:在Catamaran字体中,UPM等于1000,也就是说 。
而另一方面,CSS规范中所说的“font-size
是em的缩放因子”,其实是指font-size
的值是多少,那么1em的值就是多少。例如:font-size: 100px
,就是指 ;而如果font-size: 16px
,那么。
有没有发现,这与CSS单位中的em在很大程度上其实是同一个概念。例如:父元素的
font-size
等于16px,那么对于子元素来说,1em就等于16px。所以如果子元素设置font-size: 2em
,那么最后渲染出来的字体大小就是: 。
综上,对于Catamaran这个字体,如果设置font-size: 100px
,我们就可以得到:
也就是:
有了这个转换公式,我们就可以很简单地计算出Catamaran字体中的字母A,在font-size
等于100px时的高度等于:
类似的,还可以计算出,小写字母x的高度为48.5px;ascent的高度为110px;descent的高度为54px。
至此,我们差不多搞清楚了从矢量字形向实际字体大小转换的过程。
最后,还留下一个问题:<span>的高度为什么是164px。其实答案已经呼之欲出了。根据CSS规范中所说“UA可以使用字体的最大升部(ascender)和降部(descender)作为行内框的内容区域的高度”,而上文提到最大升部和降部其实就是ascent和descent的意思。那么,<span>的高度就是:
这其实是很合理的选择,因为理论上一个字体中的所有字形都应该在ascent和descent之间。不过实际上,ascent和descent仅仅是参考,并不是强制性的边界,所以设计师出于排版美观等因素的考虑,仍可以将某些字形超出ascent或descent之外,例如:
三、同一字体在不同平台显示的差异
我们现在已经知道,在现代浏览器中都是采用的方式来计算行内元素的内容区域的高度。但是,有些字体在不同操作系统和浏览器中显示的高度仍然可能不同。这是因为在字体中,可以用来设置ascent和descent的字段有好几组,而设计师出于某些原因可能为这几组字段分别设置了不同的值。
首先,让我们来看下有哪几组字段可以用来设置ascent和descent。
- 下表列出的是在字体文件的metadata中保存的原始的字段名,而不是字体设计软件的UI上显示的名字,这是因为不同字体设计软件会为这几个字段取不同的名字。
- 字体文件的metadata分成好几个表。
表名 | ascent字段 | descent字段 | 说明 |
---|---|---|---|
Horizontal Header table (hhea) | ascender | descender | 默认情况下,MacOS系统中的浏览器会使用这两个字段 |
Windows Metrics table (OS/2) | usWinAscent | usWinDescent | 默认情况下,Windows系统中的浏览器会使用这两个字段 |
Windows Metrics table (OS/2) | sTypoAscender | sTypoDescender | 这俩字段其实是为了解决上面MacOS和Windows取不同字段而新加的统一字段,需要通过设置fsSelection里的bit位才能开启。目前,各平台对这俩字段的支持度还不够。在MacOS中,只有Firefox会读取这俩字段。 |
接着,以Roboto这个字体为例,让我们用opentype.js的Font Inspector来查看一下Roboto的metadata。
如图,由于Roboto并未开启sTypoAscender和sTypoDescender,所以如果我们将本文开头的例子中的Catamaran字体换成Roboto字体,那么在MacOS中,<span>的高度应为:
而在Windows中,<span>的高度应为:
不过由于各浏览器对px浮点数取整的逻辑略有差异,所以最终渲染出来的高度可能有1px的偏差。
四、第一个可用字体
接下来,我还想讲一下,在计算行内元素的内容区域高度的时候,CSS规范里强调的使用第一个可用字体是什么意思。
还是让我们来看一个例子。
<span style="
font-family: 'Catamaran', 'PingFang SC', sans-serif;
font-size: 100px;
outline: 1px solid;
">哈</span>
<span style="
font-family: 'PingFang SC', sans-serif;
font-size: 100px;
outline: 1px solid;
">哈</span>
<!-- 由于PingFang SC只在Mac和iOS上可用,-->
<!-- 所以对于使用Windows或Android的读者可自行改成对应平台的中文字体,如:微软雅黑 -->
我在本文最开始的例子的基础上做了一点修改。下面的两个<span>唯一的区别就在于font-family
。我们知道,如果在font-family
后设置多个字体,那么优先级是从左往右依次降低的。换句话说,如果较左边的字体不包含某个字符,那么就会尝试使用较右边的字体,直至找到包含那个字符的字体为止。而一般来说,英文字体中并不包含中文字符。所以实际上,上图中的两个“哈”字最终都是使用PingFang SC字体来渲染的,但是,对应<span>的高度却并不相同。
这是因为第一个<span>的第一个可用字体是Catamaran,所以它的内容区域高度基于Catamaran的ascent和descent计算得到;而第二个<span>的第一个可用字体是PingFang SC,所以它的内容区域高度基于PingFang SC的ascent和descent计算得出。
五、总结
至此,本文的主要内容就结束了。让我们再来总结下本文的知识点:
font-size: 100px
中的100px等于1em的大小,并不是直观表示为页面上某段肯定的距离。- 行内元素的内容区域的高度由字体中的ascent和descent相加得到。
- 由于不同平台读取字体中不同的ascent和descent字段,所以导致行内元素的内容区域高度有所不同。
- 浏览器根据第一个可用字体的ascent和descent来计算行内元素的内容区域高度,跟实际渲染字符所用的字体无关。