搞跨端渲染?你绕不开的 HarfBuzz 原理
本文是 HarfBuzz 系列的第二篇:
本文概述
一、关键概念与结构
1.1 script
HarfBuzz 中 script 指的是文字系统的类型,注意不是指语言,不同语言也可能属于同一类书写系统,比如:
我们在从0到1自定义文字排版引擎:原理篇中有提到:
Unicode 为每个 code point 定义了一个 script 属性,文本分段时就是按 script 属性连续来划分的。
比如对于Hello世界あい,从左往右扫描字符串,每遇到 Script 改变,就切分出一个 run,最后会被划分成:
Hello→ Latin世界→ Hanあい→ Hiragana
HarfBuzz 文本塑形时,一次只能处理同一 script 的字符串,因为不同 script 的文字系统,有不同的排版规则,比如:
HarfBuzz 内部会根据 script 选取不同的 shaping 规则。
1.2 cluster
cluster 是 HarfBuzz 中的概念,表示一组不可分割的字符序列。单个字母或符号可以是一个 cluster,连字之后的字形也可以视为一个 cluster,比如f + i连字后变成fi,fi也是一个 cluster。
1.3 grapheme
grapheme 是 Unicode 中的概念,表示书写系统中的最小单位,比如单个的字母、符号、表情等,都是一个 grapheme。
cluster 与 grapheme 的区别:
二者没有相互关系,也不是一一对应的。举个例子,f + i连字后变成fi,那最终在 HarfBuzz 塑形完后只有fi 一个 cluster,但是底层仍然是 f 和 i 两个 grapheme 构成的。
HarfBuzz 中一般关注的是 cluster。
1.4 blob
blob 是一个抽象概念,表示一段二进制数据的容器,用来管理原始数据的生命周期和权限,在 HarfBuzz 中用 hb_blob_t 结构体表示。
blob 的主要作用是封装字体文件的原始数据,当我们需要加载字体传给 HarfBuzz 时,这些字体文件一般会被加载到一个 blob 对象中。
blob 只关心数据本身,不理解数据的含义,也就是说 blob 并不知道这是一个字体文件,更不知道里面有什么表,它只负责管理对应内存的生命周期,确保数据在被使用期间是可访问的。
1.5 face
face 表示一个单独的字体,它会解析 blob 中的二进制字体数据,通过 face 可以访问字体中的各种 table,如 GSUB、GPOS、cmap 表等,在 HarfBuzz 中用 hb_face_t 结构体表示。
需要注意的是,face 是不带字号、缩放和其他渲染参数的,因此 face 无法直接用于塑形。
如果要塑形的话,需要通过 face 创建 font。
1.6 font
font 表示字体实例,可以在 face 的基础上,设置字号、缩放等 feature 来创建一个 font,在 HarfBuzz 中用 hb_font_t 结构体表示。
HarfBuzz 的核心排版函数 hb_shape() 接受的是 hb_font_t 对象,而不是 hb_face_t,这是因为 HarfBuzz 在计算字形位置和前进量时,需要知道字体的大小和比例。
1.7 buffer
buffer 在 HarfBuzz 中表示输入输出的缓冲区,用 hb_buffer_t 结构体表示。
比如我们在调用塑形函数** **hb_shape() 时,我们的输入字符串及其属性(方向、script、语言等)都是通过 buffer 完成的,塑形完成后,塑形结果也会通过 buffer 将字形及位置信息返回。
1.8 user data
上面提到的 hb_blob_t、hb_face_t、hb_font_t、hb_buffer_t 结构体,都有类似set_user_data()和get_user_data()的方法,主要作用是方便携带用户上下文。
1.9 小结
前面我们介绍了 hb_blob_t、hb_face_t、hb_font_t、hb_buffer_t 等概念,这些在 HarfBuzz 中被称为对象类型(注意并非 OOP 中面向对象的概念)。
在 HarfBuzz 中,所有对象类型都提供了特定的生命周期管理 API,对象采用引用计数方式管理生命周期,通过各种 create() 方法构建,初始创建时,引用计数为 1,通过 reference() 方法引用(引用计数+1),通过 destroy() 方法解除引用(引用计数-1),当引用计数为 0 时释放。
比如,hb_buffer_t 对象可以通过 hb_buffer_create() 创建,通过 hb_buffer_reference() 引用,通过 hb_buffer_destroy() 解除引用。
HarfBuzz 所有对象的生命周期管理 API 都是线程安全的(除非你从源代码编译 HarfBuzz 时使用了HB_NO_MT配置标志),即便对象整体并非线程安全,引用(reference())或销毁(destroy())NULL 值也是允许的。
二、塑形操作
塑形大多以来字体中的 GSUB 和 GPOS 表,这一节我们来看塑形过程中的常见的操作:
1)字形替换
一对一替换(Simplified Forms):根据 feature、语言设置的不同,会进行简体 ↔ 繁体、半角 ↔ 全角、普通 ↔ 装饰体等的转换。比如在日文中会将标点符号「。」替换为它的全角版本「。」
多对一替换/连字(Standard Ligatures):比如把 f + i → 合成为 fi 连字
一对多替换/分解(Glyph Composition/Decomposition):把一个“复合”字形拆解成多个独立的字形,通常用于预处理阶段,主要为了后续定位、重音调整等更方便,一般不会影响最终的视觉效果;比如某些字体可能将预组合的 é 字形分解为
e + ´上下文替换(Initial Forms/Medial Forms/Final Forms/Isolated Forms):根据字形在单词中的位置来替换,比如阿拉伯文字母
ه会根据其在词首、词中、词尾或孤立出现,被替换为هـ、ـهـ、ـه、ه四种完全不同的形态辅音连缀(Conjunct Forms):比如
क+्+ष会被替换字形为क्ष
2)字形定位:不修改字形,但是会影响 position
字距调整(Kern):比如
T与o相邻时o的 x 偏移量为负,使它更靠近T标记定位(Mark):带重音符的如
é,会将´的前进量设为 0,并设置y_offset使其移动到e的正上方草书连接 (Cursive Attachment):在“草书”类文字中(如阿拉伯文 Arabic、叙利亚文 Syriac、南印度文等),字符之间并不是并排放置的,而是通过笔画自然地连接起来的
3)字形重排
主要用于印度系文字,比如逻辑顺序上 辅音क+ 元音ि 时,元音在视觉顺序上需要重排到辅音的左侧,即: कि
总结
本文是从 0 到 1 自定义富文本渲染的原理篇之一,此外你还可能感兴趣:
更多内容可订阅公众号:非专业程序员 Ping
版权声明: 本文为 InfoQ 作者【非专业程序员Ping】的原创文章。
原文链接:【http://xie.infoq.cn/article/278971047913e8e1352b32a17】。文章转载请联系作者。







评论