说明:
Harfbuzz 是一个开源的text opentype layout 引擎,它被应用于很多的开源项目中,如Pango,Filefox,Webkit,android等。
这份文档是Harfbuzz 的作者Behdad Esfahbod 完成用于说明新版的harfbuzz (harfbuzz-ng) API 设计思路的。
这份文档翻译自harfbuzz的邮件列表。由日期,我们可以看到,这份文档是2009年完成的,因而,这份文档其实并不能完全反映harfbuzz-ng code的当前状况,甚至可以说差异还有点大。
目前harfbuzz-ng已经被porting到Pango,Webkit等项目中,因而harfbuzz-ng 的用法,大致也可以从这些项目中找到一点蛛丝马迹。从code中的那些demo里,我们也可以学习一点harfbuzz-ng 的API。
有一些地方,实在是有些看不明白,或不知如何翻译,因而也就留原文在那里,供参考。
Behdad Esfahbod behdad at behdad.org
Tue Aug 18 16:23:50 PDT 2009
Previous message: [HarfBuzz] New Indic standard?
Next message: [HarfBuzz] HarfBuzz API design
Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
[警告: 这是一封很长的mail] Hi all, 随着重写的Harfbuzz OpenType Layout 引擎在最近被合并进pango主分支,我已经为公用API 而工作了许多个星期了。尽管仍然存在一些问题,但我已完成了大部分的设计,并很高兴能得到反馈。可以在下面的位置浏览当前的code:
http://git.gnome.org/cgit/pango/tree/pango/opentype
未来我将在那个目录下另外添加一个configure.ac文件,以使它可以作为一个独立的library来编译。两周之后,我也许会把它移回它自己的git repo,并使用git 魔法来把它pull进pango,直到我们开始把它作为一个共享库来使用(期待是在年底)。
设计HarfBuzz API 时,我参考了cairo。即是,可用性被列为最高优先级来考量。此外,隐藏技术细节、保持强大的功能而在内部实现高级特性,是API的其他一些目标。
这封mail中,我将只讨论backend-agnostic(后端不可知,无需关心API的实现的)API,那些我期待多数用户将会使用的API。也是用户通过包含"hb.h"可以访问到的那些API。例如,OpenType-specific APIs将只包含在"hb-ot.h"中,包括查询所支持的OpenType scripts, language systems, features, 等的列表API 。
最后,API的另一个严格的目标是完全的线程安全。那意味着,我不得不忍痛添加引用计数API。对象的生命周期API像cairo的,每一个对象都有: _create(), _reference(), _destory(), 和 _get_reference_count()。在某些时候,我们也许还想要添加_[gs]et_user_data(), 这对language bindings有用。
错误处理设计的也有点像cairo的,即,对象在内部记录failure (包括malloc failure), 但与cairo不同的是,没有直接的方法来查询对象的errors。HarfBuzz只是尽力去为你获取你想要的结果。但在出现errors的情况下,输出可能是错误的,但已经无法做的更好的了。总之没有很多办法来报告那种状态。所以,没有错误处理的API。
在介绍API之前,让我先来介绍一个我添加的内存管理的结构:
hb_blob_t是一个用于管理原始数据的含有引用计数的容器,为了使HarfBuzz和用户之间的内存管理变得简单和灵活而被引入。Blobs 可以像这样来创建:
typedef enum { HB_MEMORY_MODE_DUPLICATE, HB_MEMORY_MODE_READONLY, HB_MEMORY_MODE_WRITABLE, HB_MEMORY_MODE_READONLY_MAY_MAKE_WRITABLE } hb_memory_mode_t; typedef struct hb_blob_t hb_blob_t; 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);
各个不同的mode参数的含义为:
用户也可以创建一个blob的子blob:
hb_blob_t * hb_blob_create_sub_blob (hb_blob_t *parent, unsigned int offset, unsigned int length);
在锁定了Blob的数据之后就可以进行访问了:
const char * hb_blob_lock (hb_blob_t *blob);
用户可检查数据是否可写:
hb_bool_t hb_blob_is_writeable (hb_blob_t *blob);
可以在适当的地方请求将其变为可写的:
hb_bool_t hb_blob_try_writeable_inplace (hb_blob_t *blob);
或者可以请求使数据变为可写的,如果需要则创建一份拷贝:
hb_bool_t hb_blob_try_writeable (hb_blob_t *blob);
对于后一种情况,blob必须是没有被锁定的。锁是递归的。blob内部成员使用一个mutex来保护,因此这个结构是线程安全的。
blob的主要用途为提供font data或者table data给HarfBuzz。更多信息请参见后文。
Text API
也许老的基于QT HarfBuzz shaper API 的API和新的API最大的不同之处在于,新的API复用了hb-buffer同时用于shaping的输入+输出。因而,你将像下面这样使用harfbuzz:
buffer的输出是两个数组:glyph infos和glyph positions。最终这两个结构将看上去像下面这样:
typedef struct hb_glyph_info_t { hb_codepoint_t codepoint; hb_mask_t mask; uint32_t cluster; /*< private >*/ hb_var_int_t var1; hb_var_int_t var2; } hb_glyph_info_t; typedef struct hb_glyph_position_t { hb_position_t x_advance; hb_position_t y_advance; hb_position_t x_offset; hb_position_t y_offset; /*< private >*/ hb_var_int_t var; } hb_glyph_position_t;
使用hb-buffer用于输入所带来的一个好处是,现在我们可以通过实现如下接口来简单的添加UTF-8,UTF-16和UTF-32的 APIs:
void hb_buffer_add_utf8 (hb_buffer_t *buffer, const char *text, int text_length, unsigned int item_offset, int item_length); void hb_buffer_add_utf16 (hb_buffer_t *buffer, const uint16_t *text, int text_length, unsigned int item_offset, int item_length); void hb_buffer_add_utf32 (hb_buffer_t *buffer, const uint32_t *text, int text_length, unsigned int item_offset, int item_length);
它们向buffer中添加单独的Unicode字符,并分别设置cluster值。
Face
HarfBuzz是围绕着SFNT font格式而被创建起来的。一个Face简单的表示一个SFNT face,尽管这对于用户是完全透明的:你可以将无效的数据作为font data传给Harfbuzz ,但HarfBuzz会简单的忽略它。有两个主要的face构造器:
hb_face_t * hb_face_create (hb_blob_t *blob, unsigned int index); typedef hb_blob_t * (*hb_reference_table_func_t) (hb_face_t *face, hb_tag_t tag, void *user_data); /* calls destroy() when not needing user_data anymore */ hb_face_t * hb_face_create_for_tables (hb_reference_table_func_t reference_table_func, void *user_data, hb_destroy_func_t destroy);
for_tables()版本使用一个回调来load SFNT tables,而不带for_tables()的版本需要一个包含font文件数据的blob,加上TTC 集合中的face 索引。
目前face只负责shaping的“复杂的”部分,即, OpenType Layout features (GSUB/GPOS...)。未来我们也许也会直接访问cmap。现在没有实现,但老式风格的 'kern' table 将也会在想同的层次来实现。
引入blob机制的原因是,新的OpenType Layout 引擎以及我们将会添加的其他的表工作直接使用font 数据,而不是把它解析到分离的数据结构中。因此,我们需要首先"sanitize" (审查)font数据。当sanitizing(审查)时,不是仅仅给出pass/fail的结果,而是依据发现的错误(比如,一个指向了超出了table的边界的偏移量),我们也许会修改font数据以使它足够正确,从而可以传递给layout code。在那些情况下,我们首先尝试使blob变得可写,如果失败,则创建它的一个可写的副本。即简单或复杂的写时复制。对于正常的fonts,这意味着per-process的零内存消耗。未来我们将在fontconfig中缓存 sanitize()的结果,以便于不是每一个process都不得不sanitize() clean fonts。
Font
通常我宁愿font 构造器只有一个hb_face_t 参数(像cairo所做的那样)。一个font是具有某些hinting或其他选项的某一字体大小下的一个face。然而,由于FreeType缺少引用计数,而使这变得很困难。原因是:Pango基于FT_Face实例的通用槽来缓存hb_face_t。然而,一个hb_font_t应该被关联到一个PangoFont或PangoFcFont。
正如每个人都了解的,FT_Face不是线程安全的,它没有引用计数,并且也不仅仅是一个face,它还包含一个font某一时刻的字体大小的信息。由于这个原因,无论何时一个font想要访问一个FT_Face,它都需要先“lock”。虽然你lock了它,但你获取的对象不一定与上次获取的相同As everyone knows, FT_Face is not threadsafe, is not refcounted, and is not just a face, but also includes sizing information for one font at a time. For this reasons, whenever a font wants to access a FT_Face, it needs to "lock" one. When you lock it though, you don't necessarily get the same object that you got the last time. It may be a totally different object, created for the same font data, depending on who manages your FT_Face pool (cairo in our case). Anyway, for this reason, having hb_font_t have a ref to hb_face_t makes life hard: one either would have to create/destroy hb_font_t betweenFT_Face lock/unlock, or risk having a hb_face_t pointing to memory owned by a FT_Face that may have been freed since.
For the reasons above I opted for not refing a face from hb_font_t and instead passing both a face and a font around in the API. Maybe I should use a different name (hb_font_scale_t?) I'd rather keep names short, instead of cairo style hb_font_face_t and hb_scaled_font_t.
Anyway, a font is created easily:
hb_font_t * hb_font_create (hb_face_t *face);
One then needs to set various parameters on it, and after the last change, it can be used from multiple threads safely.
Shaping
当前我确定的主要的 hb_shape() API 如下:
typedef struct hb_feature_t { hb_tag_t tag; uint32_t value; unsigned int start; unsigned int end; } hb_feature_t; void hb_shape (hb_font_t *font, hb_buffer_t *buffer, const hb_feature_t *features, unsigned int num_features);
features 参数通常为空,但也可以被用于传递像下面的这些东西:
调用shape()通常需要更多的信息。也就是:
text direction,script,和language。注意,那些都不属于face 或 font 对象。对于text direction,我很确信它应该设给buffer,并且在shaping时,该值已经设置好了。
对于script和language,则稍微有点微妙。我同样确信它们属于buffer。对于script,这很好,但对于language,这引入了一个实现上的麻烦:即我将不得不处理language tag的复制/interning, 一些我尝试去避免的事情。另一些选择是:
因此,此处的问题,很欢迎收到comments。
Harfbuzz 本身不包含任何Unicode 字符的数据库表,但却需要访问一些属性,其中的一些只用于fallback shaping。目前我已经确定如下的属性在某些地方是有用的:
typedef hb_codepoint_t (*hb_unicode_mirroring_func_t)( hb_unicode_funcs_t *ufuncs, hb_codepoint_t unicode, void *user_data);
需要实现字符级的mirroring。
typedef hb_unicode_general_category_t (*hb_unicode_general_category_func_t)( hb_unicode_funcs_t *ufuncs, hb_codepoint_t unicode, void *user_data);
当face没有GDEF glyph classes时,用于合成它们。
typedef hb_script_t (*hb_unicode_script_func_t)(hb_unicode_funcs_t *ufuncs, hb_codepoint_t unicode, void *user_data);
除非我们也实现了script itemization(我们可以透明的来完成,比如,如果用户给shape()函数传入了SCRIPT_COMMON),否则我们不需要它。
typedef hb_unicode_combining_class_t (*hb_unicode_combining_class_func_t)( hb_unicode_funcs_t *ufuncs, hb_codepoint_t unicode, void *user_data);
当GPOS不可用时,为mark positioning分类有用。
typedef unsigned int (*hb_unicode_eastasian_width_func_t)( hb_unicode_funcs_t *ufuncs, hb_codepoint_t unicode, void *user_data);
不确定它在Harfbuzz 层是否有用。最近,在Pango中,垂直方向情况下我需要用它来设置正确的文本。
我已经添加了一个称为 hb_unicode_funcs_t的对象,它包含所有的这些回调。它可以被引用,也可以被复制。还有一个 hb_unicode_funcs_make_immutable() 调用,对于那些想要放出一个 它们自己拥有的hb_unicode_funcs_t对象的引用,又想要确保用户不会错误的修改那个对象的libraries有用。
然后hb-glib.h层实现:
hb_unicode_funcs_t * hb_glib_get_unicode_funcs (void);
接下来的问题是,在哪儿将unicode funcs传给shape()机制。我当前的设计是设置给face:
void hb_face_set_unicode_funcs (hb_face_t *face, hb_unicode_funcs_t *unicode_funcs);
然而那是相当武断的。face中并没有什么地方是单独需要Unicode 功能的。此外,我想要保持face的 objective。例如,你应该能够从任何可以获取一个 hb_face_t的地方(pango...)获取它,并且可以在无须担心它的设置的情况下去使用它。 Unicode funcs,虽然定义良好,但仍然可以从许多地方来获取: glib, Qt, Python的,你自己的实验,...
我开始考虑把它移到buffer里。那是仅有的 Unicode的其他的入口 (add_utf8/...),并且buffer是仅有的不被 HarfBuzz共享的对象,所以,用户对它有完全的控制权。
有人可能会问,一开始为什么要使得回调是可设置的呢?我们可以在编译时硬编码它们:如果 glib可用, 就使用它,否则使用我们自己的拷贝或其它什么东西。尽管我也许会使编译时可用的作为备用品,但是我想要使用户可以自己设置回调。最少直到我写了一个 UCD库来管理它们 ...
因而那是另外一个我需要得到反馈的问题。
这些是我已经写出原型的 font callbacks (font class, font funcs, ...)。注意, font,face,和一个 user_data 参数会同时传给它们。技术上而言,这些回调中的一些只需要一个 face,不需要 font, 但由于许多系统在实际的font,而不是face之上实现这些函数,我们需要以现在的这种方式来实现。当前我们可以给 hb-font设置 hb_font_callbacks_t对象和 user_data (hb_font_set_funcs())。
typedef hb_bool_t (*hb_font_get_glyph_func_t)(hb_font_t *font, void *font_data, hb_codepoint_t unicode, hb_codepoint_t variation_selector, hb_codepoint_t *glyph, void *user_data);这是 cmap回调。注意 variant_selector:它支持 cmap14表。对于老式的客户端,它们可以忽略那个参数,并做映射。我们也许将会内部实现对于 Unicode cmaps,但对于丢失的 glyphs或者找不到合适的 cmap的情况,可以使用这个函数。那有三个好处:
typedef hb_bool_t (*hb_font_get_glyph_contour_point_func_t)(hb_font_t *font, void *font_data, hb_codepoint_t glyph, unsigned int point_index, hb_position_t *x, hb_position_t *y, void *user_data);复杂的 GPOS positioning需要它。
Needed for complex GPOS positioning. Pango never did this before. Pretty straightforward, just need to make it clear the space that the positions are returned in. I'll discuss that in the next section.
typedef void (*hb_font_get_glyph_metrics_func_t) (hb_font_t *font, hb_face_t *face, const void *user_data, hb_codepoint_t glyph, hb_glyph_metrics_t *metrics);
This one is a bit more tricky. Technically we just need the advance width. The rest of the metrics are only used for fallback mark positioning. So maybe I should split this in a get_glyph_advance and a full get_glyph_metrics one. Current HarfBuzz has a single call to get advance width of multiple glyphs. If that kind of optimization deems necessary in the future, we can add a callback to take an entire buffer and set the advances.
现在仍然有如下问题
typedef hb_position_t (*hb_font_get_glyph_kerning_func_t)(hb_font_t *font, void *font_data, hb_codepoint_t first_glyph, hb_codepoint_t second_glyph, void *user_data);
Again, most probably we will read 'kern' table internally anyway, but this can be used for fallback with non-SFNT fonts. You can even pass, say, SVG fonts through HarfBuzz such that the higher level just deals with one API.
Another call that may be useful is a get_font_metrics one. Again, only useful in fallback positioning. In that case, ascent/descent as well as slope come handy.
目前,基于老的code,font对象具有如下的setters:
void hb_font_set_scale (hb_font_t *font, int x_scale, int y_scale); /* * A zero value means "no hinting in that direction" */ void hb_font_set_ppem (hb_font_t *font, unsigned int x_ppem, unsigned int y_ppem);
ppem API是定义明确的:那是用于 hinting和 device-dependent定位的 ppem。老的 HarfBuzz也有一个 "device-independent"设定,但那需要关掉 hinting。我已经移除那个设定以支持传递0作为 ppem。那允许在一个方向的 hinting,而不是另一个。不像老的 HarfBuzz,我们将自己做 metrics hinting。
set_scale() API在 FreeType之后建模,但使用起来仍然是笨拙的。 与HarfBuzz有关有四个不同的空间:
Now, what the hb_font_set_scale() call accepts right now is a 16.16 pair of scales mapping from font design space to device space. I'm not sure, but getting that number from font systems other than FreeType may actually be quite hard. The problem is, upem is an implementation detail of the face, and the user shouldn't care about it.
So my proposal is to separate upem and make it a face property. In fact, we can read upem from OS/2 SFNT table and assume a 1024 upem for non-SFNT fonts (that's what Type1 did IIRC). In fact, we wouldn't directly use upem for non-SFNT fonts right now. Then the scale would simply need to map EM space to device space. But notice how that's the same as the ppem. But then again, we really just care about user space for positioning (device space comes in only when hinting). So, set_scale should be changed to accept em-to-user-space scale. Not surprisingly, that's the same as the font size in the user-space.
这里我需要解决的另一个问题是, cairo允许一个完全的 matrix来完成设备空间到用户空间的转换。即,比如 glyphs可以就地的被旋转。那也是我们用来实现竖直文本的方法。我倾向于也添加一个 full-matrix setter。其行为将是:
In that model however, I wonder how easy/hard would it be for callbacks to provide requested values (contour point, glyph metrics, etc) in the user space. For cairo/pango I know that's actually the easiest thing to do, anything else would need conversion, but I'm not sure about other systems. An alternative would be to let the callbacks choose which space the returned value is in, so we can map appropriately.
I guess that's it for now. Let discussion begin. Thanks for reading!
behdad