HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】

本文概述

本文是 HarfBuzz 系列的完结篇。

本文主要结合示例来讲解HarfBuzz中的核心API,不会面面俱到,只会介绍常用和重要的。

本文是HarfBuzz系列的第三篇,在阅读本文前,推荐先阅读以下两篇文章:

1)第一篇:HarfBuzz概览

2)第二篇:HarfBuzz核心概念

更多内容在公众号「非专业程序员Ping」,此外你可能还感兴趣:

一、hb-blob

1)定义

blob 是一个抽象概念,是对一段二进制数据的封装,一般用来承载字体数据,在HarfBuzz中用 hb_blob_t 结构体表示。

2)hb_blob_create

hb_blob_t 的构造方法,签名如下:表示从一段二进制数据(u8序列)中创建

C++ 复制代码
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释放时的回调

使用示例:

Swift 复制代码
// 准备字体文件
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 的构造方法,签名如下:表示从文件路径创建

Swift 复制代码
hb_blob_t *
hb_blob_create_from_file (const char ​*file_name);
  • file_name:文件绝对路径,注意非文件名

使用示例:

Swift 复制代码
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

Swift 复制代码
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

Swift 复制代码
hb_face_t *
hb_face_reference (hb_face_t​ ​*face);

3)hb_face_destroy

hb_face_t的引用计数 -1,注意不是直接销毁对象,在HarfBuzz中,所有对象类型都提供了特定的生命周期管理API(create、reference、destroy),对象采用引用计数方式管理生命周期,当引用计数为0时才会释放内存

Swift 复制代码
void
hb_face_destroy (hb_face_t​ ​*face);

在实际使用时,需要注意调用顺序,需要保证所有从face创建出的对象销毁之后,再调用hb_face_destroy。

4)hb_face_get_upem

获取字体的upem。

Swift 复制代码
unsigned int
hb_face_get_upem (const ​hb_face_t​ ​*face);

upem 即 unitsPerEm,在字体文件中一般存储在 head 表中,字体的upem通常很大(一般是1000或2048),其单位并不是像素值,而是 em unit,<unitsPerEm value="2048"/> 表示 2048 units = 1 em = 设计的字高,比如当字体在屏幕上以 16px 渲染时,1 em = 16px,其他数值可按比例换算。

5)hb_face_reference_table

从字体中获取原始的table数据,这个函数返回的是table数据的引用,而不是拷贝,所以这个函数几乎没有性能开销;如果对应 tag 的table不存在,会返回一个空的blob,可以通过 hb_blob_get_length 来检查获取是否成功。

Swift 复制代码
hb_blob_t *
hb_face_reference_table (const ​hb_face_t​ ​*face,
                         hb_tag_t​ tag);

使用示例:

Swift 复制代码
// 构造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。

Swift 复制代码
void
hb_face_collect_unicodes (hb_face_t​ ​*face,
                          hb_set_t​ ​*out);

可以用收集好的结果来判断字体文件是否支持某个字符,这在做字体回退时非常有用。

使用示例:

Bash 复制代码
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)

C++ 复制代码
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)等的结果。

使用示例:

Swift 复制代码
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

Swift 复制代码
void
hb_font_set_ptem (hb_font_t​ ​*font,
                  float ptem);

这个函数是 hb_font_set_scale() 简易封装,在HarfBuzz内部,字体大小不是用 points 来存储的,而是用一个称为 scale 的 26.6 的整数格式来存储的。

使用示例:

Swift 复制代码
// 设置字体大小为 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),这在做字体回退时非常有用。

Swift 复制代码
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,使用哪个函数取决于字符串编码方式。

C++ 复制代码
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 表示全部长度

使用示例:

Java 复制代码
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等)。

Swift 复制代码
void
hb_buffer_guess_segment_properties (hb_buffer_t​ ​*buffer);

这个函数一般取第一个字符的属性作为整体buffer的属性,所以如果要使用这个函数来猜测属性的话,需要保证字符串已经被提前分段。

当然也可以手动调用hb_buffer_set_script、hb_buffer_set_language 等来手动设置。

五、hb-shape

hb_shape是HarfBuzz的核心塑形函数,签名如下:

C++ 复制代码
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 签名如下:

Swift 复制代码
// 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 的签名如下:

Swift 复制代码
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主要在排版/绘制时使用,以绘制为例,通常用法如下:

Swift 复制代码
// (x, y) 是"笔尖"或"光标"位置
var current_x: Double = 0.0 
var current_y: Double = 0.0

// 获取塑形结果
var glyphCount: UInt32 = 0
let infos = hb_buffer_get_glyph_infos(buffer, &glyphCount)
let positions = hb_buffer_get_glyph_positions(buffer, &glyphCount)

// 遍历所有输出的字形
for i in 0..<Int(glyphCount) {
    let info = infos[i]
    let pos = positions[i]

    // 1. 计算这个字形的绘制位置 (Draw Position)
    //    = 当前光标位置 + 本字形的偏移
    let draw_x = current_x + (Double(pos.x_offset) / 64.0)
    let draw_y = current_y + (Double(pos.y_offset) / 64.0)

    // 2. 在该位置绘制字形
    //    (info.codepoint 就是字形 ID)
    drawGlyph(glyphID: info.codepoint, x: draw_x, y: draw_y)

    // 3. 将光标移动到下一个字形的起点
    //    = 当前光标位置 + 本字形的前进位移
    current_x += (Double(pos.x_advance) / 64.0)
    current_y += (Double(pos.y_advance) / 64.0)
}

六、完整示例

下面我们以 Swift 中调用 HarfBuzz 塑形一段文本为例:

Swift 复制代码
func shapeTextExample() {
    // 1. 准备字体
    let ctFont = UIFont.systemFont(ofSize: 18) as CTFont
    let url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as! URL

    // 2. 从字体文件路径创建blob
    let blob = url.path.withCString { ptr in
        hb_blob_create_from_file(ptr)
    }

    guard let face = hb_face_create(blob, 0) else { // 0 是字体索引 (TTC/OTF collections)
        print("无法创建 HarfBuzz face。")
        hb_blob_destroy(blob) // 即使失败也要清理
        return
    }

    // Blob 已经被 face 引用,现在可以安全销毁
    hb_blob_destroy(blob)

    // --- 3. 创建 HarfBuzz 字体对象 ---
    guard let font = hb_font_create(face) else {
        print("无法创建 HarfBuzz font。")
        hb_face_destroy(face)
        return
    }

    // 告诉 HarfBuzz 使用其内置的 OpenType 函数来获取字形等信息
    // 这对于 OpenType 字体(.otf, .ttf)是必需的
    hb_ot_font_set_funcs(font)

    hb_font_set_synthetic_slant(font, 1.0)

    // 设置字体大小 (例如 100pt)。
    // HarfBuzz 内部使用 26.6 整数坐标系,即 1 单位 = 1/64 像素。
    let points: Int32 = 100
    let scale = points * 64
    hb_font_set_scale(font, scale, scale)

    // --- 4. 创建 HarfBuzz 缓冲区 ---
    guard let buffer = hb_buffer_create() else {
        print("无法创建 HarfBuzz buffer。")
        hb_font_destroy(font)
        hb_face_destroy(face)
        return
    }

    // --- 5. 添加文本到缓冲区 ---
    let text = "Hello World!"
    let cText = text.cString(using: .utf8)!

    // hb_buffer_add_utf8:
    // - buffer: 缓冲区
    // - cText: UTF-8 字符串指针
    // - -1: 字符串长度 (传 -1 表示自动计算直到 null 终止符)
    // - 0: item_offset (从字符串开头)
    // - -1: item_length (处理整个字符串)
    hb_buffer_add_utf8(buffer, cText, -1, 0, -1)

    // 猜测文本属性 (语言、文字方向、脚本)
    // 这对于阿拉伯语 (RTL - 从右到左) 至关重要!
    hb_buffer_guess_segment_properties(buffer)

    // --- 6. 执行塑形 (Shape!) ---
    // 使用 nil 特征 (features),表示使用字体的默认 OpenType 特征
    hb_shape(font, buffer, nil, 0)

    // --- 7. 获取塑形结果 ---
    var glyphCount: UInt32 = 0
    // 获取字形信息 (glyph_info)
    let glyphInfoPtr = hb_buffer_get_glyph_infos(buffer, &glyphCount)
    // 获取字形位置 (glyph_position)
    let glyphPosPtr = hb_buffer_get_glyph_positions(buffer, &glyphCount)

    guard glyphCount > 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..<Int(glyphCount) {
        let info = glyphInfo[i]
        let pos = glyphPos[i]

        let glyphID = info.codepoint // 这是字形 ID (不是 Unicode 码点!)
        let cluster = info.cluster  // 映射回原始字符串的字节索引

        let x_adv = pos.x_advance   // X 轴前进
        let y_adv = pos.y_advance   // Y 轴前进
        let x_off = pos.x_offset    // X 轴偏移 (绘制位置)
        let y_off = pos.y_offset    // Y 轴偏移 (绘制位置)

        print("Glyph[\(i)]: ID=\(glyphID)")
        print("  Cluster (string index): \(cluster)")
        print("  Advance: (x=\(Double(x_adv) / 64.0), y=\(Double(y_adv) / 64.0)) pt") // 除以 64 转回 pt
        print("  Offset:  (x=\(Double(x_off) / 64.0), y=\(Double(y_off) / 64.0)) pt")
        print("  Cursor pos before draw: (x=\(Double((currentX + x_off)) / 64.0), y=\(Double((currentY + y_off)) / 64.0)) pt")

        // 累加光标位置
        currentX += x_adv
        currentY += y_adv
    }

    print("------------------------------------------")
    print("Total Advance: (x=\(currentX / 64), y=\(currentY / 64)) pt")

    // --- 9. 清理所有 HarfBuzz 对象 ---
    // 按照创建的相反顺序销毁
    hb_buffer_destroy(buffer)
    hb_font_destroy(font)
    hb_face_destroy(face)

    print("✅ 塑形和清理完成。")
}

输出结果如下:

相关推荐
贺biubiu3 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
xuekai200809013 小时前
mysql-组复制 -8.4.7 主从搭建
android·adb
nono牛5 小时前
ps -A|grep gate
android
未知名Android用户5 小时前
Android动态变化渐变背景
android
tiantian_cool6 小时前
Claude Code 四大核心技能使用指南
ios
nono牛6 小时前
Gatekeeper 的精确定义
android
stevenzqzq8 小时前
android启动初始化和注入理解3
android
城东米粉儿10 小时前
compose 状态提升 笔记
android
冰淇淋真好吃10 小时前
iOS实现 WKWebView 长截图的优雅方案
ios
粤M温同学10 小时前
Android 实现沉浸式状态栏
android