计算机字体渲染的学问

前言:

哥们的项目中用到了树莓派Zero,就是那个搭载着约十年前单核芯片的单板计算机。性能制约令人痛苦,幸运的是它也具有GPU,于是使用了OpenGL ES、OpenVG等硬件加速框架。其中为了渲染矢量字体,接触了FreeType字体库。

读完FreeType文档之后,发现习以为常的字体里也有大学问,今天咱们来聊聊字体

字体对比

字体是什么?

正如上图,同样的内容,不一样的视觉表现形式。计算机并不需要字体来完成它的工作:查找、储存、编辑修改,但只要是通过屏幕、打印机这些视觉介质把文本呈现给人类看,就必须有字体

无论是开机BIOS打印的简陋字符,还是日常使用的微软雅黑、苹方等非常漂亮的字体,甚至咱们手写的带有个人特色的一系列文本都是字体。这么算下来,字体就是文本的视觉表现形式

欧柳颜赵,他们的字体有人模仿能以假乱真,但是计算机字体必须做到一模一样。网页缩放文本毫不失真,可截图放大净是马赛克。咱们从原理上找找原因。

讲个故事:

还记得小学我最喜欢的杂志叫做《电脑爱好者》,到现在家里还有几十本留了下来,其中有一期介绍了清华大学做的一个网站能生成个人的字体。就是在格子纸按照要求的顺序写汉字英文数字,再拍照上传,就能获得字体文件。正好我爸他是个硬笔书法爱好者,就让他写了两千多个常用字(远达不到GB2312)上传生成,最后字体真的做了出来。

保存有那个字体的电脑早已废弃不用,而且被我折腾成了Linux,虽然希望渺茫,下次回家还是要去找一找那个字体。

字体的表示原理

一般的字型表示方式有两种:位图(bitmap)、矢量轮廓(vectorial outline)。

位图

汉字取模

搞嵌入式的朋友对位图应该不陌生,在12864LCD屏幕上显示文本要先在电脑上取模,再把获得的数据写到源文件里。取模的时候要选择字体的尺寸,16*16、24*24就代表每个字占多少个像素。16*16=256,每个字256个像素,也就要256个bit来确定每一个像素上是空白(0)还是有颜色(1)。用位图来描述字型,其实就是对画面的直接表述,用的时候方便快速,直接输出到显示即可。

如图,这样的文字很有颗粒感,当然也可以制作分辨率超高的位图字体,只不过占用空间就有点大了。除此以外,字号是基本固定的,16*16的字体在12864上大小正常,放到4K屏幕就会非常小,即便像素4合1合成了更大像素,颗粒感依然非常强。要想多种字号,就要保存每个字号的字体,所以目前位图在PC手机上应用较少。

矢量

矢量字型要求在渲染的过程中进行数学运算才能得出字型。但是随着算力提升,计算机、智能手机一般都是矢量字体,Windows下的TrueType就是最常见的,结尾为ttf的文件就是TrueType矢量字体文件。

具体来说,矢量表述也用到了,但这些点并不用来显示,而是作为矢量描述的一部分。连续两点之间使用数学方法描述出字体的轮廓(如下图),一段段轮廓首尾相接形成封闭字型。

线段

字体描述中,轮廓是一段段直线曲线组成,,直的叫“线段”(line segment),曲线则是由贝塞尔曲线(Bézier arc),每条线的两端成为端点(on point,图中黑色实心),直线段是由两个on point描述的。

至于贝塞尔曲线,除了端点还需要一个或多个控制点(off point,图中空心点),一个控制点两个端点组成二次贝塞尔曲线,如果再加一个控制点就形成三次贝塞尔曲线。贝塞尔曲线在设计领域的广泛应用也是因为如此优雅简单的描述方式。

不过,当遇到复杂的字符,比如某些汉字,描述会异常复杂,也就需要大量控制点和端点。而且,这些点也要放在划分好的点位上,不能随意放置。在设计字体时,往往会先设定一个方形区域,称为EM square。这个EM区域会被划分成许多的单元(unit),单元的数量越多,可以认为设计字体时可选的点位也越多,字型也就可以更精细。一般来说TrueType字体会有2048个unit。

对比一下

位图,对字型图像的直接描述,不适合缩放。由于不需要缩放演算,用起来比较快。

矢量,对字型的轮廓描述,放缩无失真,有算力要求。

字符排版要素

应用广泛的矢量轮廓表述在实际应用中会遇到不少问题,比如屏幕是由像素组成,矢量轮廓计算再精确也逃不过最后要显示到分立的像素上去。再者,汉字较为方正,换做其他文字可能会出现需要处理的地方。看完FreeType文档后,不由感谢相关工作者的付出,平常被忽略的细节里隐藏着无数的工作成果。

example

以上图中的example为例,字母p非常的潇洒,甚至都延申到了a。文字是斜的,完全分不出字间距,amp三个字连在了一起。这已经非常像人类用手书写的文本了,但问题是,计算机是怎么做到的。

下面是计算机渲染文本时常用的一部分步骤。

字距微调(Kerning)

让我们简化一下情况,讨论比较常见的字符。

按照常见的从左到右书写方式,字符会一个挨着一个地排列,如下图:

字符排列

图中带箭头线段的长度被称为进宽度(advance width),它们的宽度各不相同。在同一行上,下一个字体开始的位置要根据前面文字的advance width来确定,但很多时候并不是单纯的相加,比如BRAVO:

nokern

kern

对比两个BRAVO,发现即使文章里的A与V都是第二种情况,在非斜体的情况下竖直方向上没有留出通路。这种样式更符合书写和阅读习惯。这样的调整被成为字距微调(kerning)。其实现原理基本就是预置一些规则表比如kern和GPOS表,当连续文本出现这些组合的时候,就按照设定调整字距。其中GPOS还能根据上下文内容来进一步优化,比如当前字符在句子中的位置。

放缩中的栅格对齐(grid-fitting,hinting)

矢量字体在渲染前必须放缩到需要的大小,再经过计算转换成像素描述的位图以便显示。按照之前说的矢量轮廓的组成方法:端点与控制点,把所有的点位置等比放大进行计算,就是需要的结果。所以,这就要求各个字符放大的中心点要一致,不然放大后字符参差不齐。

在转换成位图时,因为分辨率达不到无穷大,就会出现一些问题。比如H字母应该是对称的,但如果非常不巧,一些控制点或者端点在缩放后落不到显示器的像素栅格上,就可能会出现H两边粗细不同,E的三横长短不一这种情况。栅格对齐(grid-fitting或hinting),就会稍微变化一下字型比例,将所有的点放置到像素栅格上去,保证渲染显示不出问题。

除此以外,栅格对齐还能优化缩放后阅读观感,比如小写的m在一些低分辨率屏幕上如果缩小太多,它下部会变得非常密集影响美观和辨识度,这时hinting就会把字符横向拉长一些,下部的间距由此变大,更加美观。

总结

最近搞得东西资料总是很少,官方英文文档动辄几百页,昏天黑地的读下来可以说是痛并快乐着。

字体与显示方面知识繁多,不少内容是第一次接触,觉得值得记录,本文内容不过是九牛一毛。

带有括号的名词是自己起的名字,请以括号内英文为准。

技术新人,水平一般,文章如有错误请一定指出,如有其他意见也请不吝赐教。

参考:

FreeType官方文档:https://www.freetype.org/freetype2/docs/glyphs/index.html

OpenVG1.1官方文档:https://www.khronos.org/registry/OpenVG/specs/openvg-1.1.pdf

更多嵌入式相关内容,请来公众号,找我聊聊天吧:

公众号

欢迎转载,转载请注明作者与原文链接。

作者:胡小安

原文地址:https://www.cnblogs.com/huxiaoan/p/15147360.html

posted @ 2021-08-16 13:29  胡小安  阅读(1154)  评论(3编辑  收藏  举报