题图来自 http://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align

为什么同样的 font-size ,文字高度不一样?

每个字体在设计的时候,都是基于一个 EM Square,这是活字印刷中字模的高度。

在数字化字体中,em 是空间的数字化定义总量。在OpenType字体中,UPM或em大小通常是1000单位。在TrueType字体中,UPM约定是2的幂,通常是1024或2048。

实际情况中,许多字体的内容高度其实是比 em box 要大的。

同是 font-size: 30px 情况下,此处的 Noto Sans JP 的字体空间就比 Kosugi Maru 要高。而且在字体框内,垂直方向上还有留白。

undefined undefined

垂直方向留白大小的计算公式,可以由字体文件中的定义得到:

internal leading = ascent - descent - EM_size

代码片段

可以在这个 fiddle 里看到结果。

使用 canvas 度量文字宽度

function getMetricsByCanvas(canvas, str, font) {
const ctx = canvas.getContext('2d')
ctx.font = font
console.log(str, font, ctx.measureText(str))
}

window.onload = function() {
const canvas = document.createElement('canvas')
document.body.appendChild(canvas)
window.canvas = canvas

getMetricsByCanvas(canvas, '字', '30px Noto Sans JP') // 字 30px Noto Sans JP TextMetrics {width: 30}
}

不过此 API 是拿不到字符的高度的。就有一些比较黑的方法来估算字体的内容高度,例如使用大写字母 'M' 的宽度作为内功高度的近似。这些技巧其实都与字形设计的惯例有关,在拉丁字母中,'M' 是字形最为饱满和方正的字符,高度与宽度近似。

不过明显这个惯例对于以上两个日文字体并不适用

汉字因为字形多数为饱满的方块字,用宽度去估计内容高度其实更容易,例如 '人' 和 '口' 就很好用。

创建临时 dom 元素用于度量高度

能拿到更全的字形盒信息

function getMetricsBySpan(str, font) {
var d = document.createElement("span");
d.style.font = font;
d.textContent = str;
document.body.appendChild(d);
const emHeight = d.offsetHeight;
const emWidth = d.offsetWidth;

console.log(str, font, { emWidth, emHeight })

document.body.removeChild(d);
}

getMetricsBySpan('字', '30px Noto Sans JP') // 字 30px Noto Sans JP {emWidth: 30, emHeight: 45}

稍微不那么简单但准确的方法

基于 canvas 的 FontMetrics

FontMetrics 这个库,先清空 canvas,将文字渲染至 canvas 上,然后逐行统计 canvas 上的像素,由此可以知道文字的上下内容边界,再与 font size 换算,便可以得到字符的高度。

opentype.js

opentype.js

opentype.js 是一个优秀的解析 OpenType 字体的 js 库。以 ArrayBuffer 传入字体的数据,解析出所有 OpenType 标准数据,完全可以基于此写出符合自己需求的排版引擎。

const font = parse(buffer)
const g = font.charToGlyph('字')
const bb = g.getBoundingBox() // 得出 { x1, x2 , y1, y2 }

参考

SO 上的一个问题

FreeType Glyph Metrics

Typographic effects in canvas

Deep dive CSS: font metrics, line-height and vertical-align