harfbuzz-ng如何选择一个shaper
harfbuzz-ng在shape文字时,依然是会根据文字的特点,选中一个种shaper,然后再将实际shape的过程都交给shaper来处理这样。只是在harfbuzz-ng中,不像在老版harfbuzz中那样,是简单的依据所传入字串的script,通过一个表就直接地选定一个shaper,而是在参考字串的特性(script)之外,还会参考字库文件本身的特征,来最终选定一个shaper。同时,harfbuzz-ng中的shaper与老版harfbuzz中的shaper在概念上也有一定的区别。老版harfbuzz中的shaper基本上都是针对某种特定的语言而实现,并借助于内部的OpenType处理功能,来提供OpenType的高级渲染操作,可以称为是语言shaper吧。而在harfbuzz-ng中,其shaper则主要包括Graphite2 shaper,OpenType shaper这样的一些,可以称为是字库shaper吧。借助于harfbuzz-ng这样的一种结构,用户可以只为harfbuzz-ng编写一个客户端,然后简单的将Graphite2之类的其他shape engine接到harfbuzz-ng下面,以实现对字串的最优化shaping。harfbuzz-ng本身主要是实现了OpenType shaper,因而下面我们也会更多的关注与OpenType shaper有关的一些内容。下面我们就来看一下,在harfbuzz-ng中,选择shaper的逻辑是怎样的吧。当然,下面的code分析,一定是基于harfbuzz某个特定的版本的,这个版本实际上是0.9.10,harfbuzz目前都还依然处于开放状态中,本文的分析对于未来的某些版本也可能会有不适用的状况。
首先,来看一下harfbuzz-ng的主入口函数hb_shape():
void hb_shape (hb_font_t *font, hb_buffer_t *buffer, const hb_feature_t *features, unsigned int num_features) { hb_shape_full (font, buffer, features, num_features, NULL); }
这个函数接收font和buffer参数,其中font里面包含有与字体相关的信息,比如所使用的字库文件的内容,字体的大小等;而buffer中则包含有关于字串的信息,比如字串的内容,字串的方向和script等。而这个函数的 features 和 num_features 参数常常是NULL和0,因为客户端通常都不需要自己来确定shape一个字串所需要的features嘛,选择到底要使用那些features的工作,完全交给harfbuzz-ng来就行了。完成对字串的shaping之后,结果会通过传入的buffer参数返回给调用者。
可以看到hb_shape()的定义倒是简单的很,就只是调用了hb_shape_full()来完成所有的工作而已。接下来来看一下hb_shape_full()的实现:
hb_bool_t hb_shape_full (hb_font_t *font, hb_buffer_t *buffer, const hb_feature_t *features, unsigned int num_features, const char * const *shaper_list) { if (unlikely (!buffer->len)) return true; assert (buffer->content_type == HB_BUFFER_CONTENT_TYPE_UNICODE); buffer->guess_segment_properties (); hb_shape_plan_t *shape_plan = hb_shape_plan_create_cached (font->face, &buffer->props, features, num_features, shaper_list); hb_bool_t res = hb_shape_plan_execute (shape_plan, font, buffer, features, num_features); hb_shape_plan_destroy (shape_plan); if (res) buffer->content_type = HB_BUFFER_CONTENT_TYPE_GLYPHS; return res; }
可以看到这个函数对字串做shape的过程:
不过所谓的shape plan究竟是个什么东西呢?先来看一下hb_shape_plan_t的定义:
struct hb_shape_plan_t { hb_object_header_t header; ASSERT_POD (); hb_bool_t default_shaper_list; hb_face_t *face; hb_segment_properties_t props; hb_shape_func_t *shaper_func; const char *shaper_name; struct hb_shaper_data_t shaper_data; };
在harfbuzz-ng中,hb_face_t是一个字库文件的抽象,harfbuzz-ng可以借助于这个对象来获取字库文件的一些内容,比如获取OpenType的表等;而hb_segment_properties_t则主要保存字串属性相关的一些信息,包括script,direction和language等。可以看到,这个结构前面的几个字段,主要是与字库文件(face)和字串属性(props)有关的一些内容,而后面的几个字段,则是与选中的shaper有关的一些内容。不难理解,创建shape plan的过程,大体上应该是,用client传入的参数,设置前面的几个字段(default_shaper_list,face和props),然后依据于client传入的参数,创建或者确定后面几个字段(shaper_func,shaper_name和shaper_data)的内容。接着,我们就来看一下,harfbuzz-ng到底是如何完成这一切的吧。
前面提到,hb_shape_plan_create_cached()函数创建shape plan,那么我们就先来看一下这个函数的定义:
hb_shape_plan_t * hb_shape_plan_create_cached (hb_face_t *face, const hb_segment_properties_t *props, const hb_feature_t *user_features, unsigned int num_user_features, const char * const *shaper_list) { if (num_user_features) return hb_shape_plan_create (face, props, user_features, num_user_features, shaper_list); hb_shape_plan_proposal_t proposal = { *props, shaper_list, NULL }; if (shaper_list) { /* Choose shaper. Adapted from hb_shape_plan_plan(). */ #define HB_SHAPER_PLAN(shaper) \ HB_STMT_START { \ if (hb_##shaper##_shaper_face_data_ensure (face)) \ proposal.shaper_func = _hb_##shaper##_shape; \ } HB_STMT_END for (const char * const *shaper_item = shaper_list; *shaper_item; shaper_item++) if (0) ; #define HB_SHAPER_IMPLEMENT(shaper) \ else if (0 == strcmp (*shaper_item, #shaper)) \ HB_SHAPER_PLAN (shaper); #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT #undef HB_SHAPER_PLAN if (unlikely (!proposal.shaper_list)) return hb_shape_plan_get_empty (); } retry: hb_face_t::plan_node_t *cached_plan_nodes = (hb_face_t::plan_node_t *) hb_atomic_ptr_get (&face->shape_plans); for (hb_face_t::plan_node_t *node = cached_plan_nodes; node; node = node->next) if (hb_shape_plan_matches (node->shape_plan, &proposal)) return hb_shape_plan_reference (node->shape_plan); /* Not found. */ hb_shape_plan_t *shape_plan = hb_shape_plan_create (face, props, user_features, num_user_features, shaper_list); hb_face_t::plan_node_t *node = (hb_face_t::plan_node_t *) calloc (1, sizeof (hb_face_t::plan_node_t)); if (unlikely (!node)) return shape_plan; node->shape_plan = shape_plan; node->next = cached_plan_nodes; if (!hb_atomic_ptr_cmpexch (&face->shape_plans, cached_plan_nodes, node)) { hb_shape_plan_destroy (shape_plan); free (node); goto retry; } /* Release our reference on face. */ hb_face_destroy (face); return hb_shape_plan_reference (shape_plan); }
可以看到,这个函数正是主要依据于face和props来创建shape plan的。这个函数中为两种情况做了一些特殊的处理:第一种是num_user_features大于0的情况,即客户端指定了一些features;第二种是shaper_list非空的情况,即客户端已经提供了一个shaper列表给harfbuzz-ng来选则适当的shaper。
第一种情况简单明了,会直接调用hb_shape_plan_create()函数来创建shape plan,此处不再多说,后面会再来说明这个函数的定义。那就来看一下第二种情况的一些特殊处理:
if (shaper_list) { /* Choose shaper. Adapted from hb_shape_plan_plan(). */ #define HB_SHAPER_PLAN(shaper) \ HB_STMT_START { \ if (hb_##shaper##_shaper_face_data_ensure (face)) \ proposal.shaper_func = _hb_##shaper##_shape; \ } HB_STMT_END for (const char * const *shaper_item = shaper_list; *shaper_item; shaper_item++) if (0) ; #define HB_SHAPER_IMPLEMENT(shaper) \ else if (0 == strcmp (*shaper_item, #shaper)) \ HB_SHAPER_PLAN (shaper); #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT #undef HB_SHAPER_PLAN if (unlikely (!proposal.shaper_list)) return hb_shape_plan_get_empty (); }
这段code看起来还真够奇怪的。这都是些什么东西嘛。只是定义了两个宏,HB_SHAPER_PLAN和HB_SHAPER_IMPLEMENT,然后外加一个什么也没做的空循环,在然后就是include了一个文件而已。难道include的那个文件暗藏玄机?没错,所有的秘密还确实都在那个文件里了。我们来看一下那个hb-shaper-list.hh的内容:
#ifndef HB_SHAPER_LIST_HH #define HB_SHAPER_LIST_HH #endif /* HB_SHAPER_LIST_HH */ /* Dummy header guards */ /* v--- Add new shapers in the right place here. */ #ifdef HAVE_GRAPHITE2 /* Only picks up fonts that have a "Silf" table. */ HB_SHAPER_IMPLEMENT (graphite2) #endif #ifdef HAVE_OT HB_SHAPER_IMPLEMENT (ot) /* <--- This is our main OpenType shaper. */ #endif #ifdef HAVE_HB_OLD HB_SHAPER_IMPLEMENT (old) #endif #ifdef HAVE_ICU_LE HB_SHAPER_IMPLEMENT (icu_le) #endif #ifdef HAVE_UNISCRIBE HB_SHAPER_IMPLEMENT (uniscribe) #endif #ifdef HAVE_CORETEXT HB_SHAPER_IMPLEMENT (coretext) #endif HB_SHAPER_IMPLEMENT (fallback) /* <--- This should be last. */
至此,那两个宏的作用看起来就清晰多了嘛。根据这个文件中的内容,将前面看到的那两个宏都解开来看那段code到底是什么:
if (shaper_list) { for (const char * const *shaper_item = shaper_list; *shaper_item; shaper_item++) if (0) ; else if (0 == strcmp (*shaper_item, "graphite2")) do { if (hb_graphite_shaper_face_data_ensure (face)) proposal.shaper_func = _hb_graphite_shape; } wihle (0); else if (0 == strcmp (*shaper_item, "ot")) do { if (hb_ot_shaper_face_data_ensure (face)) proposal.shaper_func = _hb_ot_shape; } wihle (0); else if (0 == strcmp (*shaper_item, "fallback")) do { if (hb_fallback_shaper_face_data_ensure (face)) proposal.shaper_func = _hb_fallback_shape; } wihle (0); if (unlikely (!proposal.shaper_list)) return hb_shape_plan_get_empty (); }
当然,这个也不一定就是前面那段code将宏解开的真实结果。由hb-shaper-list.hh的内容,我们知道,for循环下的else-if block的数量和内容,都会依赖于到底有多少种shaper是通过宏而被enabled起来的。不过,有两种shaper是必定会被enabled的,一种是harfbuzz-ng实现的ot,另外一种就是fallback。
由这段code来看,前面所提到的针对第二种情况的特殊处理,其实也就是补足proposal中shape_func相关的信息,以便于后面在匹配shape plan时,能有更多的依据。
这段code会逐个的检查传进来的那个shaper_list中的shaper,以确定合适的shaper。它会调用shaper的hb_##shaper##_shaper_face_data_ensure()函数,比如hb_ot_shaper_face_data_ensure()等,来检查相应的shaper是否能够处理传入的那个face(字库文件),如果可以,则将相应的shaper_func函数赋给proposal.shaper_func。
那么,在选择shaper的时候为什么会需要对字库做检查呢?因为确实有一些shaper对字库有特殊的要求,比如ot的shaper就要求传入的字库必须是一个OpenType字库,而不能是简单的TrueType字库,graphite2的shaper则对字库有更高的要求。
根据需要(shaper_list非空的那段code,通常都不会执行到,shaper_list为空的时候多),对第二种情况作了proposal中shape_func相关信息的补充之后,hb_shape_plan_create_cached()函数就会从face对象中取出一个缓存的hb_face_t::plan_node_t链表(face->shape_plans),并检查是否能够找到proposal所描述的shape plan,若可以找到则将shape plan返回给调用者,创建shape plan的过程就算结束了。
若hb_shape_plan_create_cached()函数没能在face的缓存中找到所需要的shape plan的话,则它就会调用hb_shape_plan_create()来创建一个,将这个shape plan缓存进face的shape_plans链表里去,并返回刚刚创建的这个shape plan。
那个所谓的对字库的检查到底是如何进行的呢?以ot的shaper为例,来看一下字库检查都做了些什么事情。来看hb_ot_shaper_face_data_ensure()函数的定义,它是通过一个宏在相同的文件(hb-shape-plan.cc)中完成的:
#define HB_SHAPER_IMPLEMENT(shaper) \ HB_SHAPER_DATA_ENSURE_DECLARE(shaper, face) \ HB_SHAPER_DATA_ENSURE_DECLARE(shaper, font) #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT
然后,我们继续追踪,来看宏HB_SHAPER_DATA_ENSURE_DECLARE的定义(在hb-shaper-private.hh中):
#define HB_SHAPER_DATA_ENSURE_DECLARE(shaper, object) \ static inline bool \ hb_##shaper##_shaper_##object##_data_ensure (hb_##object##_t *object) \ {\ retry: \ HB_SHAPER_DATA_TYPE (shaper, object) *data = (HB_SHAPER_DATA_TYPE (shaper, object) *) hb_atomic_ptr_get (&HB_SHAPER_DATA (shaper, object)); \ if (unlikely (!data)) { \ data = HB_SHAPER_DATA_CREATE_FUNC (shaper, object) (object); \ if (unlikely (!data)) \ data = (HB_SHAPER_DATA_TYPE (shaper, object) *) HB_SHAPER_DATA_INVALID; \ if (!hb_atomic_ptr_cmpexch (&HB_SHAPER_DATA (shaper, object), NULL, data)) { \ HB_SHAPER_DATA_DESTROY_FUNC (shaper, object) (data); \ goto retry; \ } \ } \ return data != NULL && !HB_SHAPER_DATA_IS_INVALID (data); \ }这么大一坨,都是些什么东西嘛。又是引用了一堆的宏,真是看得人晕死了。不用急,可以先逐个的看一下那些宏到底是怎么定义的,然后根据那些宏的定义,再逐行的解开这个函数就都真相大白了 。首先是 HB_SHAPER_DATA, HB_SHAPER_DATA_INSTANCE和 HB_SHAPER_DATA_TYPE的定义:
#define HB_SHAPER_DATA_TYPE(shaper, object) struct hb_##shaper##_shaper_##object##_data_t #define HB_SHAPER_DATA_INSTANCE(shaper, object, instance) (* (HB_SHAPER_DATA_TYPE(shaper, object) **) &(instance)->shaper_data.shaper) #define HB_SHAPER_DATA(shaper, object) HB_SHAPER_DATA_INSTANCE (shaper, object, object)
我们知道,用于实现hb_ot_shaper_face_data_ensure()这个函数时,HB_SHAPER_DATA_ENSURE_DECLARE(shaper, object)宏的shaper是“ot”,而object是“face”。先来解开如下的这一行:
HB_SHAPER_DATA_TYPE (shaper, object) *data = (HB_SHAPER_DATA_TYPE (shaper, object) *) hb_atomic_ptr_get (&HB_SHAPER_DATA (shaper, object));
可以看到这一行实际上是:
struct hb_ot_shaper_face_data_t *data = (struct hb_ot_shaper_face_data_t *) hb_atomic_ptr_get (&(*(struct hb_ot_shaper_face_data_t**)&face->shaper_data.ot));
到底是什么意思呢?说白了就是从face对象里面拿了一个数据成员出来,即face->shaper_data.ot。那它拿的那个成员到底又是怎么一回事呢?可以再来跟一下hb_face_t定义中与这个部分有关的一些内容。来看一下那个shaper_data成员是个什么东西:
struct hb_face_t { hb_object_header_t header; ASSERT_POD (); hb_bool_t immutable; hb_reference_table_func_t reference_table_func; void *user_data; hb_destroy_func_t destroy; unsigned int index; mutable unsigned int upem; mutable unsigned int num_glyphs; struct hb_shaper_data_t shaper_data;
shaper_data的类型是hb_shaper_data_t,然后来看hb_shaper_data_t的定义:
struct hb_shaper_data_t { #define HB_SHAPER_IMPLEMENT(shaper) void *shaper; #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT };
联系我们前面看到的hb-shaper-list.hh文件的内容,可以发现hb_shaper_data_t就只是包含了一些void *类型的指针而已。 因而前面取出数据的那个过程,其实就是取出一个void *指针,然后做强制类型转换。
让我们回到HB_SHAPER_DATA_ENSURE_DECLARE的定义,接着来看HB_SHAPER_DATA_CREATE_FUNC宏的定义:
#define HB_SHAPER_DATA_CREATE_FUNC(shaper, object) _hb_##shaper##_shaper_##object##_data_create
展开这个宏就是:
_hb_ot_shaper_face_data_create
它其实是定义在hb-ot-shape.cc中的一个函数:
hb_ot_shaper_face_data_t * _hb_ot_shaper_face_data_create (hb_face_t *face) { return _hb_ot_layout_create (face); }
至此,我们可以来总结一下hb_ot_shaper_face_data_ensure()函数所做的事情:它会从face对象里面拿到对应于ot shaper的shaper_data,也就是一个struct hb_ot_shaper_face_data_t 对象,检查一下是否为空;若为空,他就会去创建一个struct hb_ot_shaper_face_data_t 对象,并赋给face->shaper_data的ot成员face->shaper_data.ot。可以再多来看一点,那个struct hb_ot_shaper_face_data_t的实际类型是struct hb_ot_layout_t:
#define hb_ot_shaper_face_data_t hb_ot_layout_t
hb_ot_shaper_face_data_ensure()函数以什么为依据来判断face所代表的字库是ot shaper所能处理的字库呢?就是看那个data对象是否能创建成功并且有效。
可见hb_ot_shaper_face_data_ensure()函数可能不仅仅是做check,它还可能新创建一个特定于shaper的结构,由face传出,以供后面shaper的func在执行时使用。
来看hb_shape_plan_create_cached()中另外的一个重要函数,也就是实际完成创建shape plan动作的hb_shape_plan_create()函数,来看它的定义:
hb_shape_plan_t * hb_shape_plan_create (hb_face_t *face, const hb_segment_properties_t *props, const hb_feature_t *user_features, unsigned int num_user_features, const char * const *shaper_list) { assert (props->direction != HB_DIRECTION_INVALID); hb_shape_plan_t *shape_plan; if (unlikely (!face)) face = hb_face_get_empty (); if (unlikely (!props || hb_object_is_inert (face))) return hb_shape_plan_get_empty (); if (!(shape_plan = hb_object_create<hb_shape_plan_t> ())) return hb_shape_plan_get_empty (); hb_face_make_immutable (face); shape_plan->default_shaper_list = shaper_list == NULL; shape_plan->face = hb_face_reference (face); shape_plan->props = *props; hb_shape_plan_plan (shape_plan, user_features, num_user_features, shaper_list); return shape_plan; }
可以看到,这个函数主要是为hb_shape_plan_t对象分配了内存空间,简单地初始化了一些变量,完了之后便调用另外的一个函数hb_shape_plan_plan()来对hb_shape_plan_t对象做更细致的设置。来看hb_shape_plan_plan()的定义:
static void hb_shape_plan_plan (hb_shape_plan_t *shape_plan, const hb_feature_t *user_features, unsigned int num_user_features, const char * const *shaper_list) { const hb_shaper_pair_t *shapers = _hb_shapers_get (); #define HB_SHAPER_PLAN(shaper) \ HB_STMT_START { \ if (hb_##shaper##_shaper_face_data_ensure (shape_plan->face)) { \ HB_SHAPER_DATA (shaper, shape_plan) = \ HB_SHAPER_DATA_CREATE_FUNC (shaper, shape_plan) (shape_plan, user_features, num_user_features); \ shape_plan->shaper_func = _hb_##shaper##_shape; \ shape_plan->shaper_name = #shaper; \ return; \ } \ } HB_STMT_END if (likely (!shaper_list)) { for (unsigned int i = 0; i < HB_SHAPERS_COUNT; i++) if (0) ; #define HB_SHAPER_IMPLEMENT(shaper) \ else if (shapers[i].func == _hb_##shaper##_shape) \ HB_SHAPER_PLAN (shaper); #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT } else { for (; *shaper_list; shaper_list++) if (0) ; #define HB_SHAPER_IMPLEMENT(shaper) \ else if (0 == strcmp (*shaper_list, #shaper)) \ HB_SHAPER_PLAN (shaper); #include "hb-shaper-list.hh" #undef HB_SHAPER_IMPLEMENT } #undef HB_SHAPER_PLAN }
又是一大堆宏在这儿绕来绕去。不过没关系,毕竟这个函数的结构总体看起来还算清晰。定义宏,然后通过include "hb-shaper-list.hh"文件而产生代码的这种手法,在前面是已经有见识过了,因而这个部分的逻辑应该也还不难理解。这个函数首先是调用了_hb_shapers_get ()函数获取到一个hb_shaper_pair_t的列表,然后分为shaper_list为空和非空两种情况来处理。先来看一下hb_shaper_pair_t的定义:
struct hb_shaper_pair_t { char name[16]; hb_shape_func_t *func; };
这个结构只有两个成员,一个是shaper的name,另外一个就是shape func,一个函数指针,其他不需要做过多的解释。
当shaper_list为空时,也是执行这个函数最经常出现的情况,在那个if block里面实现,由里面的for循环,不难看出这个函数是会从_hb_shapers_get()返回的shaper list中挑选一个。由if block里面的else-if语句,可以知道,for循环每遍历到一个shaper,就总有一个else-if能与之匹配,所以else-if语句仅有的作用,就只是帮助它的HB_SHAPER_PLAN()来识别一个shaper而已。而选择shaper的主要依据,还得看HB_SHAPER_PLAN()宏,展开这个宏的定义,可以发现,此处也一样是调用hb_##shaper##_shaper_face_data_ensure()函数来对字库文件做检查,而这个函数算是我们的老朋友了,如前所述,它主要是创建一个face data,若创建成功,harfbuzz-ng就认为相应的shaper是可用的。
选择一个shaper的具体含义又是什么呢?可以看宏HB_SHAPER_PLAN()接下来的几行,首先是,通过一个函数_hb_##shaper##_shaper_##object##_data_create(),创建一个shape_plan的data,并赋值给shape_plan->shaper_data.shaper。比如,对于ot shaper,就是调用_hb_ot_shaper_shape_plan_data_create()函数,创建一个struct hb_ot_shaper_shape_plan_data_t对象,并赋值给shape_plan->shaper_data.ot,以返回给调用者。接下来便是设置shape_plan的shaper_func为对应shaper的shaper_func。最后就是将shape_plan的shaper_name设置为对应shaper的shaper_name。
那当shaper_list非空时,又是怎样的一个执行过程呢?有相应的block里面的code来看,它与shaper_list为空时,有两点区别,一是,它在调用者传进来的shaper_list中来选择;二是,它是通过shaper的shaper_name类识别一个shaper的,而不像前面的case,是通过shaper_func来识别一个shaper。其他则都完全一样。
此处我们来总结一下,harfbuzz-ng选择一个shaper的过程。首先,harfbuzz-ng是通过调用shaper的hb_##shaper##_shaper_face_data_ensure()函数来确定那个shaper是否可用的,这个函数实际上算是在对字库文件做检查,它会创建一个face data,若创建成功,则认为相应的shaper可用,否则,认为shaper不可用。这个函数还会将创建的face data赋值给face->shaper_data.shaper,以返回给调用者。确定了一个shaper可用之后,harfbuzz-ng还会通过调用shaper的_hb_##shaper##_shaper_shape_plan_data_create()函数创建一个shape plan的data,并通过shape_plan->shaper_data.shaper返回给调用者。然后就是为shape_plan设置适当的shaper_func和shaper_name,其中的shaper_func是名为_hb_##shaper##_shape的函数。另外,就是harfbuzz-ng在选择shaper时是有按一定的优先级的,在hb-shaper-list.hh文件中,被列出的越靠前的shaper,其优先级就相应的越高。