感觉从事计算机相关行业久了,不惯是写代码的还是做设计的,应该都免不了要跟字体打交道,一些基本的字体格式(ttf)也都还算了解,知道一个不同的 ttf 可以渲染出不同形状的文字。
这个时候问题来了,现在在我们的左边是存储了各种字形信息的字体文件,在我们右边是已经渲染出好的文字。在这中间发生了什么?
在参考了一系列博客、官方文档,并阅读调试了 Typr.js 源码后,我得以一窥字体的渲染方式,并将他写下来。本文将涵盖一下内容:
关于字体文件(ttf)标准的一些了解可以参考 这篇文章。我在 中解读了具体怎么解析一个 ttf 文件。我们只需要了解一个 ttf 可以看成是一个 List[Dict],即包含多个字典(table)的列表,每个字典里的值可以是数字、文本、数组等。
使用 fonttools 可以方便的直接解析一个 ttf 文件。为了方便比对和调试,我以 github/Typr.js/demo/LiberationSans-Bold.ttf 这一英文字体为例。
其中四个字节长度的是字体文件中所有的表名,这是由字体文件的标准决定的,表名以四个字节长度大端存储。
所以你能看到有一个表名是 'cvt ',
GlyphOrder则是该库自己解析出来的。
如 TTF 文件探秘 所说:
整体来看 TTF 文件,我们可以学到一些高密度存储数据的方式
fonttools 自行优化了一些读取方式,这使我们用 fonttools 能够更容易的获取到表的一些数据了。
此时,可以对照官方文档 参考这些表的意义,这里简单列几个
cmap: character to glyph mapping
通过打印 cmap 中的 table 我们能看到 cmap 存储了一个 index 到 character name 的映射:
这些 index 实际是 unicode 编码,在 python 中我们可以通过 ord 或者 encode('utf8') 来转换:
我们可以基于此拿到相应的 character name,比如 ',A' -> [44, 65] -> ['comma', 'A']
glyf: glyph data
由于 fonttools 已经帮我们处理好了映射关系,所以我们只可以直接根据 character name 找到字形数据
我们还可以根据 glyphOrder 找出 character name 在原本字体文件中 glyphs 中的顺序:
字形数据 fonttools 帮我们处理成了字典,但没有帮我们处理成直观的格式:
但没关系,这一部分逻辑在 Typr.js:_parseGlyf 中有相应的实现。我用 Python 复刻了其中的算法,将其最终转换为了 SVG path:
那这样是不是就大功告成了呢?我们将 ',A' 同时转换成 svg path,然后构建 svg 文件看看效果
然而结果并不是很理想,有两个问题:
首先是颠倒的问题,比较好解决:y 坐标轴全取复数即可
其次是间距的问题。
hmtx: horizontal metrics kern: kerning
我们可以看到 hmtx 为每个 character name 存储了两个信息:
而 kern 则为每两个 character name 存储了一个长度:
其中 hmtx 定义了每个字的宽度,而 kern 则定义了一些特殊字形(比如上标 å )的偏移距离。
综上所述,我们就可以用这两个表计算出一个字符在一串字符中应该处于哪个位置。
便于展示,这里省略了换行情况下 y 轴的处理:
至此,我们完成了从 unicode 字符到 SVG 图片的渲染,单从这一步来看,并没有多少技术难度,更多的是对字体文件标准的把握,作为入门,我觉得已经够了。
所有上文中提到的代码统一整合后放在 ttf-render-demo