[HarfBuzz] HarfBuzz API 設計

說明: android

Harfbuzz 是一個開源的text opentype layout 引擎,它被應用於不少的開源項目中,如Pango,Filefox,Webkit,android等。 git

這份文檔是Harfbuzz 的做者Behdad Esfahbod 完成用於說明新版的harfbuzz (harfbuzz-ng) API 設計思路的。 數據庫

這份文檔翻譯自harfbuzz的郵件列表。由日期,咱們能夠看到,這份文檔是2009年完成的,於是,這份文檔其實並不能徹底反映harfbuzz-ng code的當前情況,甚至能夠說差別還有點大。 後端

目前harfbuzz-ng已經被porting到Pango,Webkit等項目中,於是harfbuzz-ng 的用法,大體也能夠從這些項目中找到一點蛛絲馬跡。從code中的那些demo裏,咱們也能夠學習一點harfbuzz-ng 的API。 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的其餘一些目標。  app

這封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
相關文章
相關標籤/搜索