写点什么

搞跨端渲染?你绕不开的 HarfBuzz 原理

  • 2025-10-27
    北京
  • 本文字数:2373 字

    阅读完需:约 8 分钟

本文是 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连字后变成也是一个 cluster。

1.3 grapheme

grapheme 是 Unicode 中的概念,表示书写系统中的最小单位,比如单个的字母、符号、表情等,都是一个 grapheme。


cluster 与 grapheme 的区别:


二者没有相互关系,也不是一一对应的。举个例子,f + i连字后变成,那最终在 HarfBuzz 塑形完后只有 一个 cluster,但是底层仍然是 fi 两个 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):比如To 相邻时 o 的 x 偏移量为负,使它更靠近 T

  • 标记定位(Mark):带重音符的如é,会将 ´ 的前进量设为 0,并设置 y_offset 使其移动到 e 的正上方

  • 草书连接 (Cursive Attachment):在“草书”类文字中(如阿拉伯文 Arabic、叙利亚文 Syriac、南印度文等),字符之间并不是并排放置的,而是通过笔画自然地连接起来的


3)字形重排


主要用于印度系文字,比如逻辑顺序上 辅音+ 元音ि 时,元音在视觉顺序上需要重排到辅音的左侧,即: कि

总结

本文是从 0 到 1 自定义富文本渲染的原理篇之一,此外你还可能感兴趣:



更多内容可订阅公众号:非专业程序员 Ping

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2019-03-12 加入

还未添加个人简介

评论

发布
暂无评论
搞跨端渲染?你绕不开的HarfBuzz原理_大前端_非专业程序员Ping_InfoQ写作社区