[HarfBuzz] HarfBuzz API 设计

说明:

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之前,让我先来介绍一个我添加的内存管理的结构:

Blobs

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参数的含义为:

  • DUPLICATE: 立即复制数据并拥有它。 
  • READONLY: 传入的数据可以被抓住并将在随后使用,但是不应该被修改。如果需要修改,blob将会简单的复制数据。 
  • WRITEABLE: 数据是可写的,可自由地使用。 
  • READONLY_NEVER_DUPLICATE: 数据是只读的,并且不应被复制。这禁掉了需要对数据进行写访问的操作。 
  • READONLY_MAY_MAKE_WRITEABLE: 数据是只读的,但是可通过使用mprotect()或等价的win32 调用来使其成为可写的。它由用户来确保对数据调用mprotect()或特定于系统的等价的接口是安全的。实际上,在Linux和(根据Tor) win32上,那从来都不会称为一个问题。 

用户也可以创建一个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     
  • 向buffer中添加文本 ---> 现在buffer中包含Unicode文本     
  • 将buffer作为参数调用 hb_shape() 
  • 使用输出的glyphs ---> 现在buffer中包含位置经过调整的glyphs 
上面描述的流程中,涉及到Harfbuzz 中如下三个主要的对象    
  • hb_buffer_t: 保存 文本/glyphs,并且不是线程安全的 
  • hb_face_t: 代表单个SFNT face,完全的线程安全的,映射到cairo_font_face_t. 
  • hb_font_t: 代表具有某一hinting选项某一字体大小下的face,完全的线程安全的,映射到cairo_scaled_font_t。 
Buffer

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 参数通常为空,但也可以被用于传递像下面的这些东西: 

  • "kern"=>"0" -------> no kerning  
  • "ot:aalt"=>"2" -------> use 2nd OpenType glyph alternative  
  • "ot:mkmk"=>"0" -------> never apply 'mkmk' OpenType feature  
Perhaps:    
  • "ot:script"=>"math" ------> Force an OpenType script tag  
  • "ot:langsys"=>"FAR " -----> Force an OpenType language system  
Maybe:     
  • "ot"=>"0" ------> Disable OpenType engine (prefer AAT, SIL, etc)  
  • 或也许甚至是标记文本的可视边界的features等。

Discussion  

Script and language 

调用shape()通常需要更多的信息。也就是:

text direction,script,和language。注意,那些都不属于face 或 font 对象。对于text direction,我很确信它应该设给buffer,并且在shaping时,该值已经设置好了。

对于script和language,则稍微有点微妙。我同样确信它们属于buffer。对于script,这很好,但对于language,这引入了一个实现上的麻烦:即我将不得不处理language tag的复制/interning, 一些我尝试去避免的事情。另一些选择是: 

  • 为hb_shape()添加额外的参数。我宁愿不这样做。在主API 之外保持这样的细节,在适当的位置添加setters使API 更干净,也更可扩展。 
  •  为它们使用feature dict。我非常反对这种做法。对于我来说,feature dict已经太highlevel了。 

因此,此处的问题,很欢迎收到comments。

Unicode callbacks 

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 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的情况,可以使用这个函数。那有三个好处: 
  • Pango etc can pass whatever code they want for missing glyphs, to use later to draw hexbox, 
  • Pango, through fontconfig, knows how to handle non-Unicode cmaps, so that will continue to work,  
  • For non-SFNT fonts, HarfBuzz should happily sit back and make things work still, this is how that will work.  

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. 

现在仍然有如下问题 

  1. The metrics struct most probably should be public. However, in the future I like to use bearing-deltas to improve positioning. A transparent struct doesn't help in those situations. Not sure what the alternatives are.  
  2. It's not exactly clear how to deal with vertical fonts. One way would be to assume that if buffer direction is vertical, then the font already knows that and returns the vertical metrics. That's not a completely off assumption, though that may not be how win32 fonts work?  

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. 

Font scale, etc 

目前,基于老的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有关有四个不同的空间: 

  • Font设计空间:典型的,是每个 glyph一个 1024x1024的盒子。 GPOS 及 'kern'的值都是在这个空间定义的。它通过称为 upem (units per em)的 per-face值被映射到 EM空间。 
  • EM 空间: 1em = 1em.  
  • 设备空间: 实际的象素。 如果一个映射存在的话,ppem将 EM空间映射到这个空间。 
  • 用户空间:用户期待 glyph将被放置的空间。它可以不同于设备空间(即,比如,如果你使用了 cairo_scale())。当前的/老的 pango忽略这个区别,并因此 kerning无法被正确的放缩 [1]。

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。其行为将是: 

  • If (1,0) maps to (x,y) with nonzero y, then many kinds of positioning should be completely disabled,  
  • Somehow figure out what to do with vertical. Not sure right now, but it should be ok detecting if the font is 90-degree rotated and compensate for that.  

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  

[1] http://bugzilla.gnome.org/show_bug.cgi?id=341481


从http://my.oschina.net/wolfcs/blog/107635转载过来,写的特别的好。值得学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值