[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轉載過來,寫的特別的好。值得學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章