本文概述 
 
本文是 HarfBuzz 系列的完结篇。 
本文主要结合示例来讲解HarfBuzz中的核心API,不会面面俱到,只会介绍常用和重要的。 
本文是HarfBuzz系列的第三篇,在阅读本文前,推荐先阅读以下两篇文章: 
1)第一篇:HarfBuzz概览 
2)第二篇:HarfBuzz核心概念 
更多内容在公众号「非专业程序员Ping」,此外你可能还感兴趣: 
 
- 一文读懂字符与编码
 
 - 一文读懂字符、字形、字体
 
 - 一文读懂字体文件
 
 - 从0到1自定义文字排版引擎:原理篇
 
 - 逆向分析CoreText中的字体级联/Font Fallback机制
 
 - 新手小白也能看懂的LLDB技巧/逆向技巧
 
  一、hb-blob 
 
1)定义 
blob 是一个抽象概念,是对一段二进制数据的封装,一般用来承载字体数据,在HarfBuzz中用 hb_blob_t 结构体表示。 
2)hb_blob_create 
hb_blob_t 的构造方法,签名如下:表示从一段二进制数据(u8序列)中创建- hb_blob_t *
 - hb_blob_create (const char *data,
 -                 unsigned int length,
 -                 hb_memory_mode_t mode,
 -                 void *user_data,
 -                 hb_destroy_func_t destroy);
 
  复制代码 
- data:原始二进制数据,比如字体文件内容
 
 - length:二进制长度
 
 - mode:内存管理策略,即如何管理二进制数据,一般使用 HB_MEMORY_MODE_DUPLICATE 最安全,类型如下
 
  模式含义优缺点HB_MEMORY_MODE_DUPLICATE复制模式,HarfBuzz会将传入的数据完整复制一份到私有内存优点是不受传入的 data 生命周期影响缺点是多一次内存分配HB_MEMORY_MODE_READONLY只读模式,HarfBuzz会直接使用传入的数据,数据不会被修改优点是无额外性能开销缺点是外部需要保证在 hb_blob_t 及其衍生的所有对象(如 hb_face_t)被销毁之前,始终保持有效且内容不变HB_MEMORY_MODE_WRITABLE可写模式,HarfBuzz会直接使用传入的指针,同时修改这块内存数据,优点同READONLY缺点同READONLY,同时还可能修改数据HB_MEMORY_MODE_READONLY_MAY_MAKE_WRITABLE写时复制,HarfBuzz会直接使用传入的指针,在需要修改这块内存时才复制一份到私有内存优点同READONLY缺点同READONLY,同时还可能修改数据 
 
- user_data:可以通过 user_data 携带一些上下文
 
 - destroy:blob释放时的回调
 
  使用示例:- // 准备字体文件
 - let ctFont = UIFont.systemFont(ofSize: 18) as CTFont
 - let url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as! URL
 - guard let fontData = try? Data(contentsOf: url) else {
 -     return
 - }
 - // 创建 HarfBuzz Blob 和 Face
 - // 'withUnsafeBytes' 确保指针在 'hb_blob_create' 调用期间是有效的。
 - // 'HB_MEMORY_MODE_DUPLICATE' 告诉 HarfBuzz 复制数据,这是在 Swift 中管理内存最安全的方式。
 - let blob = fontData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> OpaquePointer? in
 -     let charPtr = ptr.baseAddress?.assumingMemoryBound(to: CChar.self)
 -     return hb_blob_create(charPtr, UInt32(fontData.count), HB_MEMORY_MODE_DUPLICATE, nil, nil)
 - }
 
  复制代码 3)hb_blob_create_from_file 
hb_blob_t 的构造方法,签名如下:表示从文件路径创建- hb_blob_t *
 - hb_blob_create_from_file (const char *file_name);
 
  复制代码 
- file_name:文件绝对路径,注意非文件名
 
  使用示例:- let ctFont = UIFont.systemFont(ofSize: 18) as CTFont
 - let url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as! URL
 - let blob = url.path.withCString { ptr in
 -     hb_blob_create_from_file(ptr)
 - }
 
  复制代码 查看 hb_blob_create_from_file 函数实现,会通过 mmap 的方式来映射字体文件,可以共享系统的字体内存缓存,相比自己读取二进制数据来创建blob来说,这种方式会少一次IO,且内存占用也可能更小(复用系统内存缓存)。 
二、hb-face 
 
1)定义 
face 表示一个单独的字体,它会解析blob中的二进制字体数据,通过face可以访问字体中的各种table,如GSUB、GPOS、cmap表等,在HarfBuzz中用 hb_face_t 结构体表示。 
2)hb_face_create 
hb_face_t的构造方法,签名如下:表示从一段字体二进制数据中构造face- hb_face_t *
 - hb_face_create (hb_blob_t *blob,
 -                 unsigned int index);
 
  复制代码 
- blob:字体数据
 
 - index:有的字体文件是一个字体集合(ttc),index表示使用第几个字体数据来创建face;对于单字体文件(ttf)来说,index传0即可
 
 
 关于字体更多知识可以参考:一文读懂字体文件 
 3)hb_face_reference 
hb_face_t的引用计数 +1- hb_face_t *
 - hb_face_reference (hb_face_t *face);
 
  复制代码 3)hb_face_destroy 
hb_face_t的引用计数 -1,注意不是直接销毁对象,在HarfBuzz中,所有对象类型都提供了特定的生命周期管理API(create、reference、destroy),对象采用引用计数方式管理生命周期,当引用计数为0时才会释放内存。- void
 - hb_face_destroy (hb_face_t *face);
 
  复制代码 在实际使用时,需要注意调用顺序,需要保证所有从face创建出的对象销毁之后,再调用hb_face_destroy。 
4)hb_face_get_upem 
获取字体的upem。- unsigned int
 - hb_face_get_upem (const hb_face_t *face);
 
  复制代码 upem 即 unitsPerEm,在字体文件中一般存储在 head 表中,字体的upem通常很大(一般是1000或2048),其单位并不是像素值,而是 em unit, 表示 2048 units = 1 em = 设计的字高,比如当字体在屏幕上以 16px 渲染时,1 em = 16px,其他数值可按比例换算。 
5)hb_face_reference_table 
从字体中获取原始的table数据,这个函数返回的是table数据的引用,而不是拷贝,所以这个函数几乎没有性能开销;如果对应 tag 的table不存在,会返回一个空的blob,可以通过 hb_blob_get_length 来检查获取是否成功。- hb_blob_t *
 - hb_face_reference_table (const hb_face_t *face,
 -                          hb_tag_t tag);
 
  复制代码 使用示例:- // 构造tag,这里是获取head表
 - let headTag = "head".withCString { ptr in
 -     hb_tag_from_string(ptr, -1)
 - }
 - let headBlob = hb_face_reference_table(face, headTag);
 - // 检查是否成功
 - if (hb_blob_get_length(headBlob) > 0) {
 -     // 获取原始数据指针并解析
 -     var length: UInt32 = 0
 -     let ptr = hb_blob_get_data(headBlob, &length);
 -     // ... 在这里执行自定义解析 ...
 - }
 - // 必须销毁返回的 blob!
 - hb_blob_destroy(headBlob);
 
  复制代码 6)hb_face_collect_unicodes 
获取字体文件支持的所有Unicode,这个函数会遍历cmap表,收集cmap中定义的所有code point。- void
 - hb_face_collect_unicodes (hb_face_t *face,
 -                           hb_set_t *out);
 
  复制代码 可以用收集好的结果来判断字体文件是否支持某个字符,这在做字体回退时非常有用。 
使用示例:- let set = hb_set_create()
 - hb_face_collect_unicodes(face, set)
 - var cp: UInt32 = 0
 - while hb_set_next(set, &cp) == 1 {
 -     print("code point: ", cp)
 - }
 - hb_set_destroy(set)
 
  复制代码 三、hb-font 
 
1)定义 
font 表示字体实例,可以在face的基础上,设置字号、缩放等feature来创建一个font,在HarfBuzz中用 hb_font_t 结构体表示。 
2)hb_font_create & hb_font_reference & hb_font_destroy 
hb_font_t 的创建、引用、销毁函数,整体同face对象一样,采用引用计数的方式管理生命周期。 
3)hb_font_get_glyph_advance_for_direction 
获取一个字形在指定方向上的默认前进量(advance)- void
 - hb_font_get_glyph_advance_for_direction
 -                                (hb_font_t *font,
 -                                 hb_codepoint_t glyph,
 -                                 hb_direction_t direction,
 -                                 hb_position_t *x,
 -                                 hb_position_t *y);
 
  复制代码 
- font:指定字体
 
 - glyph:目标字形
 
 - direction:指定方向,HB_DIRECTION_LTR/HB_DIRECTION_LTR/HB_DIRECTION_TTB/HB_DIRECTION_BTT
 
 - x:返回值,advance.x
 
 - y:返回值,advance.y
 
  这个函数会从 hmtx(横向)或vmtx(纵向)表中读取advance。 
一般情况下,我们不需要直接使用这个函数,这个函数是直接查表返回静态的默认前进量,但实际塑形时,一般还涉及kerning等调整,所以一般常用hb_shape()的返回值,hb_shape()返回的是包含字形上下文调整(如kerning)等的结果。 
使用示例:- let glyph_A: hb_codepoint_t = 65
 - var x_adv: hb_position_t = 0
 - var y_adv: hb_position_t = 0
 - // 1. 获取 'A' 在水平方向上的前进位移
 - hb_font_get_glyph_advance_for_direction(font,
 -                                         glyph_A,
 -                                         HB_DIRECTION_LTR, // 水平方向
 -                                         &x_adv,
 -                                         &y_adv)
 
  复制代码 4)hb_font_set_ptem & hb_font_get_ptem 
设置和获取字体大小(point size),ptem 即 points per Em,也就是 iOS 中的 point size- void
 - hb_font_set_ptem (hb_font_t *font,
 -                   float ptem);
 
  复制代码 这个函数是 hb_font_set_scale() 简易封装,在HarfBuzz内部,字体大小不是用 points 来存储的,而是用一个称为 scale 的 26.6 的整数格式来存储的。 
使用示例:- // 设置字体大小为 18 pt
 - hb_font_set_ptem(myFont, 18.0f);
 - // 等价于
 - // 手动计算 scale
 - int32_t scale = (int32_t)(18.0f * 64); // scale = 1152
 - // 手动设置 scale
 - hb_font_set_scale(myFont, scale, scale);
 
  复制代码 Q:什么是 26.6 整数格式? 
"26.6" 格式是一种定点数(Fixed-Point Number)表示法,用于将浮点数转换成整数存储和运算;在 HarfBuzz 中,这个格式用于 hb_position_t 类型(int32_t),用来表示所有的坐标和度量值(如字形位置、前进量等)。 
26.6 表示将一个 32 位整数划分为:高26位用于存储整数部分(一个有符号的 25 位整数 + 1 个符号位)+ 低6位用于存储小数部分。 
换算规则:2^6 = 64 
 
- 从「浮点数」转为「26.6 格式」:hb_position_t = (float_value * 64)
 
 - 从「26.6 格式」转回「浮点数」:float_value = hb_position_t / 64.0
 
  那为什么不直接用整数呢,因为文本布局需要极高的精度,如果只用整数,那任何小于1的误差都会被忽略,在一行文本中累计下来,误差就很大了。 
那为什么不直接用浮点数呢,因为整数比浮点数的运算快,且浮点数在不同平台上存储和计算产生的误差还确定。 
因此为了兼顾性能和精确,将浮点数「放大」成整数参与计算。 
5)hb_font_get_glyph 
用于查询指定 unicode 在字体中的有效字形(glyph),这在做字体回退时非常有用。- hb_bool_t
 - hb_font_get_glyph (hb_font_t *font,
 -                    hb_codepoint_t unicode,
 -                    hb_codepoint_t variation_selector,
 -                    hb_codepoint_t *glyph);
 
  复制代码 
- 返回值 hb_bool_t:true 表示成功,glyph 被设置有效字形,false 表示失败,即字体不支持该 unicode
 
 - font:字体
 
 - unicode:待查询 unicode
 
 - variation_selector:变体选择符的code point,比如在 CJK 中日韩表意文字中,一个汉字可能有不同的字形(如下图),一个字体可能包含这些所有的变体,那我们可以通过 variation_selector 指定要查询哪个变体;如果只想获取默认字形,那该参数可传 0
 
 
  
 
- glyph:返回值,用于存储 unicode 对应字形
 
  当然,还有与之对应的批量查询的函数:hb_font_get_nominal_glyphs 
四、hb-buffer 
 
1)定义 
buffer 在HarfBuzz中表示输入输出的缓冲区,用 hb_buffer_t 结构体表示,一般用于存储塑形函数的输入和塑形结束的输出。 
2)hb_buffer_create & hb_buffer_reference & hb_buffer_destroy 
hb_buffer_t 的创建、引用、销毁函数,整体同face对象一样,采用引用计数的方式管理生命周期。 
3)hb_buffer_add_utf8 & hb_buffer_add_utf16 & hb_buffer_add_utf32 
将字符串添加到buffer,使用哪个函数取决于字符串编码方式。- void
 - hb_buffer_add_utf8 (hb_buffer_t *buffer,
 -                     const char *text,
 -                     int text_length,
 -                     unsigned int item_offset,
 -                     int item_length);
 
  复制代码 
- buffer:目标buffer
 
 - text:文本
 
 - text_length:文本长度,传 -1 会自动查找到字符串末尾的 \0
 
 - item_offset:偏移量,0 表示从头开始
 
 - item_length:添加长度,-1 表示全部长度
 
  使用示例:- let buffer = hb_buffer_create()
 - let text = "Hello World!"
 - let cText = text.cString(using: .utf8)!
 - hb_buffer_add_utf8(buffer, cText, -1, 0, -1)
 
  复制代码 4)hb_buffer_guess_segment_properties 
猜测并设置buffer的塑形属性(script、language、direction等)。- void
 - hb_buffer_guess_segment_properties (hb_buffer_t *buffer);
 
  复制代码 这个函数一般取第一个字符的属性作为整体buffer的属性,所以如果要使用这个函数来猜测属性的话,需要保证字符串已经被提前分段。 
当然也可以手动调用hb_buffer_set_script、hb_buffer_set_language 等来手动设置。 
五、hb-shape 
 
hb_shape是HarfBuzz的核心塑形函数,签名如下:- void
 - hb_shape (hb_font_t *font,
 -           hb_buffer_t *buffer,
 -           const hb_feature_t *features,
 -           unsigned int num_features);
 
  复制代码 
- font:用于塑形的字体实例,需要提前设置好字体大小等属性
 
 - buffer:既是输入,待塑形的字符串会通过buffer传入;也是输出,塑形完成后,塑形结果会通过buffer返回
 
 - features:feature数组,用于启用或禁用字体中的某些特性,不需要的话可以传nil
 
 - num_features:上一参数features数组的数量
 
  hb_shape 会执行一系列复杂操作,比如: 
 
- 字符到字形映射:查询cmap表,将字符转换为字形
 
 - 字形替换:查询 GSUB 表,进行连字替换、上下文替换等
 
 - 字形定位:查询 GPOS 表,微调每个字形的位置,比如kerning,标记定位,草书连接等
 
  详细的塑形操作可以参考HarfBuzz核心概念。 
下面重点介绍塑形结果,可以通过 hb_buffer_get_glyph_infos 和 hb_buffer_get_glyph_positions 从buffer中获取塑形结果。 
hb_buffer_get_glyph_infos 签名如下:- // hb_buffer_get_glyph_infos
 - hb_glyph_info_t *
 - hb_buffer_get_glyph_infos (hb_buffer_t *buffer,
 -                            unsigned int *length);
 - typedef struct {
 -   hb_codepoint_t codepoint;
 -   uint32_t       cluster;
 - } hb_glyph_info_t;
 
  复制代码 hb_buffer_get_glyph_infos 返回一个 hb_glyph_info_t 数组,用于获取字形信息,hb_glyph_info_t 中有两个重要参数: 
 
- codepoint:glyphID,注意这里不是 unicode 码点
 
 - cluster:映射回原始字符串的字节索引
 
  这里需要展开介绍下cluster: 
 
- 在连字 (多对一)情况下:比如 "f" 和 "i" (假设在索引 0 和 1) 被塑形为一个 "fi" 字形。这个 "fi" 字形的 cluster 值会是 0(即它所代表的第一个字符的索引)
 
 - 拆分 (一对多)情况下:在某些语言中,一个字符可能被拆分为两个字形,这两个字形都会有相同的 cluster 值,都指向那个原始字符
 
 - 高亮与光标:当我们需要高亮显示原始文本的第 3 到第 5 个字符时,就是通过 cluster 值来查找所有 cluster 在 3 和 5 之间的字形,然后绘制它们的选区
 
  hb_buffer_get_glyph_positions 的签名如下:- hb_glyph_position_t *
 - hb_buffer_get_glyph_positions (hb_buffer_t *buffer,
 -                                unsigned int *length);
 -                                
 - typedef struct {
 -   hb_position_t  x_advance;
 -   hb_position_t  y_advance;
 -   hb_position_t  x_offset;
 -   hb_position_t  y_offset;
 - } hb_glyph_position_t;
 
  复制代码 hb_buffer_get_glyph_positions 返回一个 hb_glyph_position_t 的数组,用于获取字形的位置信息,hb_glyph_position_t 参数有: 
 
- x_advance / y_advance:x / y 方向的前进量;前进量指的是绘制完一个字形后,光标应该移动多远继续绘制下一个字形;对于横向排版而言,y_advance 一般是0;需要注意的是 advance 值中已经包含了 kernig 的计算结果
 
 - x_offset / y_offset:x / y 方向的绘制偏移,对于带重音符的字符如 é 来说,塑形时可能拆分成 e + ´,重音符 ´ 塑形结果往往会带 offset,以保证绘制在 e 的上方
 
  position主要在排版/绘制时使用,以绘制为例,通常用法如下: 
[code]// (x, y) 是“笔尖”或“光标”位置var current_x: Double = 0.0 var current_y: Double = 0.0// 获取塑形结果var glyphCount: UInt32 = 0let infos = hb_buffer_get_glyph_infos(buffer, &glyphCount)let positions = hb_buffer_get_glyph_positions(buffer, &glyphCount)// 遍历所有输出的字形for i in 0.. 0, let glyphInfo = glyphInfoPtr, let glyphPos = glyphPosPtr else {        print("塑形失败或没有返回字形。")        // 清理        hb_buffer_destroy(buffer)        hb_font_destroy(font)        hb_face_destroy(face)        return    }    print("\n--- 塑形结果 for '\(text)' (\(glyphCount) glyphs) ---")    // --- 8. 遍历并打印结果 ---    // 'cluster' 字段将字形映射回原始 UTF-8 字符串中的字节索引。    // 这对于高亮显示、光标定位等非常重要。    var currentX: Int32 = 0    var currentY: Int32 = 0    // 注意:阿拉伯语是从右到左 (RTL) 的。    // hb_buffer_get_direction(buffer) 会返回 HB_DIRECTION_RTL。    // HarfBuzz 会自动处理布局,所以我们只需按顺序迭代字形。    for i in 0.. |