本文概述
本文是 HarfBuzz 系列的完结篇。
本文主要结合示例来讲解 HarfBuzz 中的核心 API,不会面面俱到,只会介绍常用和重要的。
本文是 HarfBuzz 系列的第三篇,在阅读本文前,推荐先阅读以下两篇文章:
1)第一篇:HarfBuzz概览
2)第二篇:HarfBuzz核心概念
一、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);
复制代码
使用示例:
// 准备字体文件let ctFont = UIFont.systemFont(ofSize: 18) as CTFontlet url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as! URLguard 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);
复制代码
使用示例:
let ctFont = UIFont.systemFont(ofSize: 18) as CTFontlet url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as! URLlet 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);
复制代码
关于字体更多知识可以参考:一文读懂字体文件
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 时才会释放内存。
voidhb_face_destroy (hb_face_t *face);
复制代码
在实际使用时,需要注意调用顺序,需要保证所有从 face 创建出的对象销毁之后,再调用 hb_face_destroy。
4)hb_face_get_upem
获取字体的 upem。
unsigned inthb_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 来检查获取是否成功。
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。
voidhb_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 = 0while 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)
voidhb_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);
复制代码
这个函数会从 hmtx(横向)或 vmtx(纵向)表中读取 advance。
一般情况下,我们不需要直接使用这个函数,这个函数是直接查表返回静态的默认前进量,但实际塑形时,一般还涉及 kerning 等调整,所以一般常用hb_shape()的返回值,hb_shape()返回的是包含字形上下文调整(如 kerning)等的结果。
使用示例:
let glyph_A: hb_codepoint_t = 65var x_adv: hb_position_t = 0var 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
voidhb_font_set_ptem (hb_font_t *font, float ptem);
复制代码
这个函数是 hb_font_set_scale() 简易封装,在 HarfBuzz 内部,字体大小不是用 points 来存储的,而是用一个称为 scale 的 26.6 的整数格式来存储的。
使用示例:
// 设置字体大小为 18 pthb_font_set_ptem(myFont, 18.0f);
// 等价于// 手动计算 scaleint32_t scale = (int32_t)(18.0f * 64); // scale = 1152// 手动设置 scalehb_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
那为什么不直接用整数呢,因为文本布局需要极高的精度,如果只用整数,那任何小于 1 的误差都会被忽略,在一行文本中累计下来,误差就很大了。
那为什么不直接用浮点数呢,因为整数比浮点数的运算快,且浮点数在不同平台上存储和计算产生的误差还确定。
因此为了兼顾性能和精确,将浮点数「放大」成整数参与计算。
5)hb_font_get_glyph
用于查询指定 unicode 在字体中的有效字形(glyph),这在做字体回退时非常有用。
hb_bool_thb_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
当然,还有与之对应的批量查询的函数: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,使用哪个函数取决于字符串编码方式。
voidhb_buffer_add_utf8 (hb_buffer_t *buffer, const char *text, int text_length, unsigned int item_offset, int item_length);
复制代码
使用示例:
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 等)。
voidhb_buffer_guess_segment_properties (hb_buffer_t *buffer);
复制代码
这个函数一般取第一个字符的属性作为整体 buffer 的属性,所以如果要使用这个函数来猜测属性的话,需要保证字符串已经被提前分段。
当然也可以手动调用 hb_buffer_set_script、hb_buffer_set_language 等来手动设置。
五、hb-shape
hb_shape 是 HarfBuzz 的核心塑形函数,签名如下:
voidhb_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_infoshb_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 中有两个重要参数:
这里需要展开介绍下 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 主要在排版/绘制时使用,以绘制为例,通常用法如下:
// (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..<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 塑形一段文本为例:
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("✅ 塑形和清理完成。")}
复制代码
输出结果如下:
评论